feat: add intent to apply icon packs externally - #6643
Conversation
📝 WalkthroughWalkthroughAdded an exported Activity ( Changes
Sequence DiagramsequenceDiagram
participant IconPackApp as IconPackApp
participant ApplyActivity as ApplyActivity
participant PrefViewModel as PrefViewModel
participant Resolver as Resolver
participant Prefs as Prefs
participant UI as UI
IconPackApp->>ApplyActivity: startActivity(intent with packageName)
ApplyActivity->>ApplyActivity: validate packageName extra
ApplyActivity->>PrefViewModel: read iconPackIntents
ApplyActivity->>Resolver: resolve matching activity, label, icon (async)
Resolver-->>ApplyActivity: label + icon
ApplyActivity->>UI: show confirmation bottom sheet with icon & name
UI-->>ApplyActivity: user action (Apply / Cancel / Dismiss)
alt user confirms
ApplyActivity->>Prefs: set iconPackPackage = packageName
Prefs-->>ApplyActivity: persisted
end
ApplyActivity->>ApplyActivity: finish()
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 📋 Issue PlannerLet us write the prompt for your AI agent so you can ship faster (with fewer bugs). View plan for ticket: ✨ Finishing Touches✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
a308b97 to
8014ee0
Compare
|
Thank you for the contribution. Once this is merged, we should also do PRs on these icon pack dashboards to add support for it:
Same for lawnicons of course: https://github.com/lawnchairlauncher/lawnicons |
475563b to
4a5e94e
Compare
wellorbetter
left a comment
There was a problem hiding this comment.
ApplyIconPackActivity receives the intent, validates the package against iconPackIntents, shows a confirmation dialog, and writes to iconPackPackage via PreferenceManager — same path as the Settings UI. No extra abstractions, minimal visibility change (private → internal on iconPackIntents)
SuperDragonXD
left a comment
There was a problem hiding this comment.
Hello, and thanks for the PR! I have a few suggestions to improve the UI of this feature.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt (1)
72-73:⚠️ Potential issue | 🟡 MinorReturn early when the package extra is missing.
finish()only schedules the activity to end, so without areturnthe rest ofonCreatestill executes:setContentis invoked, theLaunchedEffectlaunches,resolveIconPackInfo("")iterates alliconPackIntentsand callsqueryIntentActivities, andfinish()is called again. For the invalid/missing-package path the issue spec requires finishing silently, so this wastes work (and briefly composes UI) on a hostile external invocation path.🛠️ Proposed fix
- val packPackageName = intent.getStringExtra(EXTRA_PACKAGE_NAME).orEmpty() - if (packPackageName.isEmpty()) finish() + val packPackageName = intent.getStringExtra(EXTRA_PACKAGE_NAME).orEmpty() + if (packPackageName.isEmpty()) { + finish() + return + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt` around lines 72 - 73, The early-exit when the package extra is missing currently calls finish() but continues executing onCreate; update the check around intent.getStringExtra(EXTRA_PACKAGE_NAME) (packPackageName) to immediately return after calling finish() so the rest of onCreate (setContent, LaunchedEffect, resolveIconPackInfo and any queryIntentActivities) does not execute for the invalid/missing-package path; ensure the change is applied in ApplyIconPackActivity.onCreate so no UI is composed or background work launched when packPackageName is empty.
🧹 Nitpick comments (1)
lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt (1)
88-108: Redundant scrim: drop the full-screenSurfacewrapper.
ModalBottomSheetalready renders its own scrim (defaulting toBottomSheetDefaults.ScrimColor) over the hosting window. Wrapping it in aSurface(fillMaxSize(), color = BottomSheetDefaults.ScrimColor)stacks another scrim-colored layer beneath the sheet's scrim, producing a doubly-dimmed backdrop. Since the activity's window is translucent by virtue of being behind aModalBottomSheet, thisSurfaceisn't providing a needed background either.♻️ Proposed simplification
- LawnchairTheme { - EdgeToEdge() - Surface( - modifier = Modifier.fillMaxSize(), - color = BottomSheetDefaults.ScrimColor, - ) { - ApplyIconPackSheet( - packName = info.first, - packIcon = info.second, - onConfirm = { - PreferenceManager.getInstance(this@ApplyIconPackActivity) - .iconPackPackage.set(packPackageName) - finish() - }, - onDismiss = { finish() }, - ) - } - } + LawnchairTheme { + EdgeToEdge() + ApplyIconPackSheet( + packName = info.first, + packIcon = info.second, + onConfirm = { + PreferenceManager.getInstance(this@ApplyIconPackActivity) + .iconPackPackage.set(packPackageName) + finish() + }, + onDismiss = { finish() }, + ) + }(The
Surface/fillMaxSize/BottomSheetDefaultsimports on lines 29, 37 and thefillMaxSizeimport on line 26 can be removed if nothing else depends on them.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt` around lines 88 - 108, The Surface(...) wrapper that fills the screen with BottomSheetDefaults.ScrimColor is redundant because ModalBottomSheet already provides a scrim; in ApplyIconPackActivity remove the full-screen Surface (and its Modifier.fillMaxSize() / color = BottomSheetDefaults.ScrimColor) so ApplyIconPackSheet is placed directly inside the LawnchairTheme/EdgeToEdge block, and then clean up any now-unused imports (Surface, fillMaxSize, BottomSheetDefaults) at the top of the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt`:
- Around line 72-73: The early-exit when the package extra is missing currently
calls finish() but continues executing onCreate; update the check around
intent.getStringExtra(EXTRA_PACKAGE_NAME) (packPackageName) to immediately
return after calling finish() so the rest of onCreate (setContent,
LaunchedEffect, resolveIconPackInfo and any queryIntentActivities) does not
execute for the invalid/missing-package path; ensure the change is applied in
ApplyIconPackActivity.onCreate so no UI is composed or background work launched
when packPackageName is empty.
---
Nitpick comments:
In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt`:
- Around line 88-108: The Surface(...) wrapper that fills the screen with
BottomSheetDefaults.ScrimColor is redundant because ModalBottomSheet already
provides a scrim; in ApplyIconPackActivity remove the full-screen Surface (and
its Modifier.fillMaxSize() / color = BottomSheetDefaults.ScrimColor) so
ApplyIconPackSheet is placed directly inside the LawnchairTheme/EdgeToEdge
block, and then clean up any now-unused imports (Surface, fillMaxSize,
BottomSheetDefaults) at the top of the file.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a9cb1f41-bb73-42e2-a6d4-eff40d7b9e61
📒 Files selected for processing (1)
lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt
I'd like to suggest make this optional. When people open an icon pack app, they're usually want to look at the icons or make an icon request—not to close a pop-up they didn't ask for. A simple fix would be to make this toggleable (either by the developer or the end user) and to include a dedicated 'Apply icon pack' button within the icon pack app ui. I don't want people opening Lawnicons to deal with unnecessary sheet. |
Thanks for the concern! This doesn't show any pop-up on app launch. The implementation adds a button in the toolbar that See the Lawnicons side implementation here: LawnchairLauncher/lawnicons#3602 |
c685fba to
2e07ebd
Compare
Add ApplyIconPackActivity that handles app.lawnchair.APPLY_ICONS intent, allowing third-party icon pack apps to directly apply their icon pack to Lawnchair without requiring users to navigate to settings. - Register activity with intent-filter in AndroidManifest - Validate icon pack before showing confirmation dialog - Reuse iconPackIntents from PreferenceViewModel
- Move resolveIconPackInfo() into LaunchedEffect with Dispatchers.Default to avoid PM queries and loadIcon() on the main thread - Replace non-null assertions with smart cast
- Remove early return after finish() - Replace AlertDialog with ModalBottomSheet + ModalBottomSheetContent - Use PreferenceGroup + PreferenceTemplate for icon pack display - Align button style with existing bottom sheet patterns
ModalBottomSheet already renders its own scrim, the extra Surface with ScrimColor produced a doubly-dimmed backdrop.
2e07ebd to
7cea544
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt (2)
79-86: UseDispatchers.IOfor PackageManager queries.
PackageManager.queryIntentActivities,loadLabel, andloadIconare I/O-bound (IPC + potential resource loading from the other app's APK). PreferDispatchers.IOoverDispatchers.Defaultto avoid tying up CPU-bound workers.♻️ Proposed change
- LaunchedEffect(packPackageName) { - val result = withContext(Dispatchers.Default) { - resolveIconPackInfo(packPackageName) - } + LaunchedEffect(packPackageName) { + val result = withContext(Dispatchers.IO) { + resolveIconPackInfo(packPackageName) + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt` around lines 79 - 86, The coroutine dispatched inside the LaunchedEffect uses Dispatchers.Default for PackageManager work; change it to Dispatchers.IO so I/O-bound operations in resolveIconPackInfo (and any calls to PackageManager.queryIntentActivities, loadLabel, loadIcon) run on the IO dispatcher: update the withContext call in the LaunchedEffect that references packPackageName / resolveIconPackInfo to use Dispatchers.IO, leaving the rest of the logic (assigning packInfo, setting resolved, calling finish()) unchanged.
95-101: Animate the sheet away before finishing on confirm.
onConfirmcallsfinish()synchronously, so the bottom sheet pops out abruptly without the dismissal animation. Consider hiding the sheet state first (or deferringfinish()via a coroutine tied tosheetState.hide()) so Confirm and Cancel behave consistently in terms of UX.Also worth noting: once the preference write completes, there's no user-visible feedback (toast/snackbar) that the pack was applied — minor, but could feel opaque since the activity simply vanishes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt` around lines 95 - 101, The onConfirm lambda currently writes the preference then calls finish() immediately, causing the bottom sheet to close abruptly; instead launch a coroutine (e.g., using lifecycleScope) to first call sheetState.hide() and await its completion, then perform PreferenceManager.getInstance(this@ApplyIconPackActivity).iconPackPackage.set(packPackageName) and finally call finish(); also consider showing a short user-visible confirmation (Toast or Snackbar) after the hide completes and before finish to indicate the pack was applied.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt`:
- Around line 79-86: The coroutine dispatched inside the LaunchedEffect uses
Dispatchers.Default for PackageManager work; change it to Dispatchers.IO so
I/O-bound operations in resolveIconPackInfo (and any calls to
PackageManager.queryIntentActivities, loadLabel, loadIcon) run on the IO
dispatcher: update the withContext call in the LaunchedEffect that references
packPackageName / resolveIconPackInfo to use Dispatchers.IO, leaving the rest of
the logic (assigning packInfo, setting resolved, calling finish()) unchanged.
- Around line 95-101: The onConfirm lambda currently writes the preference then
calls finish() immediately, causing the bottom sheet to close abruptly; instead
launch a coroutine (e.g., using lifecycleScope) to first call sheetState.hide()
and await its completion, then perform
PreferenceManager.getInstance(this@ApplyIconPackActivity).iconPackPackage.set(packPackageName)
and finally call finish(); also consider showing a short user-visible
confirmation (Toast or Snackbar) after the hide completes and before finish to
indicate the pack was applied.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b6e6fb7e-8a5e-44d5-b415-800421307834
📒 Files selected for processing (4)
lawnchair/AndroidManifest.xmllawnchair/res/values/strings.xmllawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.ktlawnchair/src/app/lawnchair/ui/preferences/PreferenceViewModel.kt
✅ Files skipped from review due to trivial changes (2)
- lawnchair/res/values/strings.xml
- lawnchair/AndroidManifest.xml
🚧 Files skipped from review as they are similar to previous changes (1)
- lawnchair/src/app/lawnchair/ui/preferences/PreferenceViewModel.kt
There was a problem hiding this comment.
🧹 Nitpick comments (2)
lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt (2)
65-105: Consider callingsetResult()beforefinish()for external callers.Since the activity is
exported=trueand can be launched withstartActivityForResult, third-party icon pack apps have no way to distinguish apply vs. cancel vs. invalid-package. CallingsetResult(RESULT_OK)on confirm andsetResult(RESULT_CANCELED)on dismiss/invalid package is a small improvement that makes the public intent contract more useful.♻️ Suggested change
val packPackageName = intent.getStringExtra(EXTRA_PACKAGE_NAME).orEmpty() if (packPackageName.isEmpty()) { + setResult(RESULT_CANCELED) finish() return } @@ packInfo = result resolved = true - if (result == null) finish() + if (result == null) { + setResult(RESULT_CANCELED) + finish() + } } @@ onConfirm = { PreferenceManager.getInstance(this@ApplyIconPackActivity) .iconPackPackage.set(packPackageName) + setResult(RESULT_OK) finish() }, - onDismiss = { finish() }, + onDismiss = { + setResult(RESULT_CANCELED) + finish() + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt` around lines 65 - 105, The activity currently just calls finish() for confirm/dismiss/invalid-package; update ApplyIconPackActivity so it calls setResult(RESULT_OK) before finish() when the user confirms (inside the onConfirm handler where PreferenceManager.iconPackPackage is set) and call setResult(RESULT_CANCELED) before finish() both in the onDismiss handler and where you early-return when EXTRA_PACKAGE_NAME is empty or resolveIconPackInfo(packPackageName) returns null (use the same spot after LaunchedEffect resolves). This ensures callers using startActivityForResult can distinguish confirm vs cancel/invalid while keeping existing behavior.
95-99: Move preference write to background thread for consistency with this PR's pattern.The
onConfirmcallback invokesiconPackPackage.set(packPackageName)directly on the main thread. WhileSharedPreferences.apply()is asynchronous (non-blocking), this PR already establishes a pattern of moving I/O operations likePackageManager.queryIntentActivities()toDispatchers.IO(line 80). Applying the same pattern here would be more consistent with the codebase intent.♻️ Suggested change
- onConfirm = { - PreferenceManager.getInstance(this@ApplyIconPackActivity) - .iconPackPackage.set(packPackageName) - finish() - }, + onConfirm = { + lifecycleScope.launch { + withContext(Dispatchers.IO) { + PreferenceManager.getInstance(this@ApplyIconPackActivity) + .iconPackPackage.set(packPackageName) + } + finish() + } + },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt` around lines 95 - 99, The onConfirm callback in ApplyIconPackActivity currently calls PreferenceManager.getInstance(this@ApplyIconPackActivity).iconPackPackage.set(packPackageName) on the main thread; move the preference write to a background coroutine using Dispatchers.IO (e.g., launch(Dispatchers.IO) or withContext(Dispatchers.IO)) so the set call runs off the UI thread, then return to the main thread to call finish() if needed; update the onConfirm block to perform the iconPackPackage.set(packPackageName) inside an IO coroutine and ensure finish() executes on the main dispatcher afterwards.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt`:
- Around line 65-105: The activity currently just calls finish() for
confirm/dismiss/invalid-package; update ApplyIconPackActivity so it calls
setResult(RESULT_OK) before finish() when the user confirms (inside the
onConfirm handler where PreferenceManager.iconPackPackage is set) and call
setResult(RESULT_CANCELED) before finish() both in the onDismiss handler and
where you early-return when EXTRA_PACKAGE_NAME is empty or
resolveIconPackInfo(packPackageName) returns null (use the same spot after
LaunchedEffect resolves). This ensures callers using startActivityForResult can
distinguish confirm vs cancel/invalid while keeping existing behavior.
- Around line 95-99: The onConfirm callback in ApplyIconPackActivity currently
calls
PreferenceManager.getInstance(this@ApplyIconPackActivity).iconPackPackage.set(packPackageName)
on the main thread; move the preference write to a background coroutine using
Dispatchers.IO (e.g., launch(Dispatchers.IO) or withContext(Dispatchers.IO)) so
the set call runs off the UI thread, then return to the main thread to call
finish() if needed; update the onConfirm block to perform the
iconPackPackage.set(packPackageName) inside an IO coroutine and ensure finish()
executes on the main dispatcher afterwards.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0ad6d95e-f561-4d23-87e1-ddeecf88b3b1
📒 Files selected for processing (1)
lawnchair/src/app/lawnchair/ui/ApplyIconPackActivity.kt
SuperDragonXD
left a comment
There was a problem hiding this comment.
Thanks! Merging this now.
Summary
Implements #6594 — adds support for applying icon packs via an external intent (
app.lawnchair.APPLY_ICONS), allowing third-party icon pack apps to directly apply their icon pack to Lawnchair.Changes
ApplyIconPackActivity— handles the intent, validates the icon pack, and shows a Material3 confirmation dialogAndroidManifest.xml— registers the activity withexported=trueand intent-filterstrings.xml— adds dialog title/message stringsPreferenceViewModel.kt— makesiconPackIntentsinternal for reuseHow it works
Intent("app.lawnchair.APPLY_ICONS").putExtra("packageName", pkgName)Testing
lawnWithQuickstepGithubDebugCloses #6594
Summary by CodeRabbit
New Features
UI
Localization