Fix duplicate custom app icons - #5609
Conversation
|
Claude finished @joashrajin's task in 3m 45s —— View job Code Review — PR #5609: Fix duplicate custom app icons
Nice work — this targets the actual root cause. The real bug was that
A few points worth addressing / confirming before merge: 1. Overlap with the existing app-start sync — worth a clarifying comment (low)
2. Brief double-icon window during migration (expected, but confirm)For an existing user with a custom icon, immediately after update both 3. Inert leftover intent-filter on
|
Project manifest changes for appThe following changes in the --- ./build/reports/diff_manifest/app/release/base_manifest.txt 2026-08-07 15:03:04.564234611 +0000
+++ ./build/reports/diff_manifest/app/release/head_manifest.txt 2026-08-07 15:03:08.836546381 +0000
@@ -118,12 +118,6 @@
android:launchMode="singleTop"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan" >
<intent-filter>
- <action android:name="android.intent.action.MAIN" />
-
- <category android:name="android.intent.category.LAUNCHER" />
- <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
- </intent-filter>
- <intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -433,10 +427,6 @@
<data android:path="/get" />
<data android:pathPrefix="/get/" />
</intent-filter>
-
- <meta-data
- android:name="android.app.shortcuts"
- android:resource="@xml/shortcuts" />
</activity>
<activity-alias
@@ -450,7 +440,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_17"
@@ -463,7 +458,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_16"
@@ -476,7 +476,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_15"
@@ -489,7 +494,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_14"
@@ -502,7 +512,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_13"
@@ -515,7 +530,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_12"
@@ -528,7 +548,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_11"
@@ -541,7 +566,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_10"
@@ -554,7 +584,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_9"
@@ -567,7 +602,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_8"
@@ -580,7 +620,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_7"
@@ -593,7 +638,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_6"
@@ -606,7 +656,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_5"
@@ -619,7 +674,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_4"
@@ -632,7 +692,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_3"
@@ -645,7 +710,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_2"
@@ -658,7 +728,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_1"
@@ -671,11 +746,16 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity-alias
- android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivity_0"
- android:enabled="false"
+ android:name="au.com.shiftyjelly.pocketcasts.ui.MainActivityDefault"
+ android:enabled="true"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -684,7 +764,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
+ <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
+
+ <meta-data
+ android:name="android.app.shortcuts"
+ android:resource="@xml/shortcuts" />
</activity-alias>
<activity
@@ -720,6 +805,13 @@
</intent-filter>
</receiver>
<receiver
+ android:name="au.com.shiftyjelly.pocketcasts.AppIconUpdateReceiver"
+ android:exported="false" >
+ <intent-filter>
+ <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
+ </intent-filter>
+ </receiver>
+ <receiver
android:name="androidx.mediarouter.media.MediaTransferReceiver"
android:exported="true" >
</receiver>Go to https://buildkite.com/automattic/pocket-casts-android/builds/18198/canvas?sid=019fdcbd-3866-4e87-aa27-20c6c7ffd9e0, click on the |
- Rename settings_app_icon_updated_message to settings_app_icon_updated_message_v2 and drop the stale translations so other locales fall back to the new English copy instead of the old text that told users to expect two launcher icons - Remove the now-inert MAIN-only intent filter from MainActivity (LAUNCHER lives on the activity aliases) - Document why AppIconUpdateReceiver exists alongside the app-start alias sync - Link the changelog entry to the PR instead of the issue
|
I'll analyze this and get back to you. |
|
Claude finished @joashrajin's task in 2m 34s —— View job Code Review — PR #5609: Fix duplicate custom app icons
Re-reviewed the branch after the ✅ Verified correct
Non-blocking notes
Overall: correct root-cause fix, good manifest hygiene, and focused tests. No blocking issues from my side. The main confirmations rest on the on-device behavior you already captured (dirty-upgrade migration to a single alias with selection preserved), which the screenshots and |
|
Version |
…icons # Conflicts: # CHANGELOG.md
|
Claude finished @joashrajin's task in 7m 28s —— View job Code Review — PR #5609: Fix duplicate custom app icons
Re-reviewed after the merge of ✅ Verified
Inline comments
Non-blocking observations
Overall: correct root-cause fix, clean manifest hygiene, and the migration path for both previously-default and previously-custom users is handled properly. The three inline items are refinements — the KDoc correction is the one I'd actually want done, since a future maintainer reading it could remove the wrong half of the pair. |
| /** | ||
| * Re-syncs the launcher activity aliases right after an app update, without waiting for the user | ||
| * to open the app. [PocketCastsApplication] performs the same sync on every app start, so this | ||
| * receiver's main job is waking the process after MY_PACKAGE_REPLACED; keep both in place. |
There was a problem hiding this comment.
The stated rationale is inverted: it's the broadcast delivery that starts the process, and Application.onCreate() always runs to completion before any component callback. PocketCastsApplication.onCreate() → setupApp() (PocketCastsApplication.kt:173) synchronously calls appIcon.enableSelectedAlias(appIcon.activeAppIcon) inside runBlocking (PocketCastsApplication.kt:221), so by the time onReceive runs the aliases have already been re-synced. The receiver isn't "waking the process" — it's issuing a second, identical PackageManager batch on the main thread right after the first.
Two reasonable options:
- Drop the receiver entirely (smaller manifest surface, one fewer main-thread
PackageManagerbatch per app update). - Keep it as a belt-and-braces backstop for the case where
setupApp()throws before reaching the sync, but reword the comment so it doesn't mislead a future reader into thinking the receiver is what triggers the migration.
| @Test | ||
| @Config(sdk = [33]) | ||
| fun `aliases are updated atomically when supported`() { | ||
| appIcon.enableSelectedAlias(AppIconType.DARK) | ||
|
|
||
| val settingsCaptor = argumentCaptor<List<PackageManager.ComponentEnabledSetting>>() | ||
| verify(packageManager).setComponentEnabledSettings(settingsCaptor.capture()) | ||
| verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any()) | ||
|
|
||
| val states = settingsCaptor.firstValue.associate { setting -> | ||
| requireNotNull(setting.componentName).className to setting.enabledState | ||
| } | ||
| assertEquals(AppIconType.entries.size, states.size) | ||
| assertEquals(PackageManager.COMPONENT_ENABLED_STATE_ENABLED, states[DARK_ALIAS]) | ||
| assertEquals(1, states.values.count { it == PackageManager.COMPONENT_ENABLED_STATE_ENABLED }) | ||
| } |
There was a problem hiding this comment.
Coverage gap: the atomic (API 33+) path is only exercised for a custom icon (DARK), while DEFAULT is only tested on API 32. The default case is precisely where the old code was wrong (it disabled every alias and relied on MainActivity carrying LAUNCHER), so it's the case most worth pinning down on the path that the majority of devices will actually take. A parameterised variant, or just one extra test, would close it:
@Test
@Config(sdk = [33])
fun `default icon enables only the default alias when updated atomically`() {
appIcon.enableSelectedAlias(AppIconType.DEFAULT)
val settingsCaptor = argumentCaptor<List<PackageManager.ComponentEnabledSetting>>()
verify(packageManager).setComponentEnabledSettings(settingsCaptor.capture())
val states = settingsCaptor.firstValue.associate { requireNotNull(it.componentName).className to it.enabledState }
assertEquals(PackageManager.COMPONENT_ENABLED_STATE_ENABLED, states[DEFAULT_ALIAS])
assertEquals(1, states.values.count { it == PackageManager.COMPONENT_ENABLED_STATE_ENABLED })
}| android:resource="@xml/shortcuts"/> | ||
| </activity> | ||
|
|
||
| <activity-alias |
There was a problem hiding this comment.
The 19 aliases are now identical apart from android:name and android:icon, and their names have to stay in lockstep with AppIcon.AppIconType.aliasName in a different module (modules/services/ui). Drift between those two lists is the exact shape of the bug this PR fixes (MainActivity_0 permanently disabled while MainActivity carried LAUNCHER), and nothing currently fails the build if a future icon is added to the enum but not the manifest — setComponentEnabledSetting on a non-existent component throws IllegalArgumentException at runtime.
Worth adding a cheap guard, e.g. an androidTest in app that walks the enum and asserts each alias really exists and is wired up:
val pkg = context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_ACTIVITIES or PackageManager.MATCH_DISABLED_COMPONENTS,
)
val declared = pkg.activities.orEmpty().map { it.name }.toSet()
AppIcon.AppIconType.entries.forEach { type ->
assertTrue("Missing alias for $type", "au.com.shiftyjelly.pocketcasts${type.aliasName}" in declared)
}(For reference, I verified the current state is consistent: 19 enum entries ↔ 19 aliases, each with LAUNCHER, MULTIWINDOW_LAUNCHER, and the android.app.shortcuts meta-data.)
Unrelated nit two lines up: removing the android.app.shortcuts meta-data from MainActivity left a stray blank line before </activity> at line 326.
|
Version |

Description
Pocket Casts currently exposes both MainActivity and the selected activity alias as launcher entries. On affected launchers, especially Samsung One UI, selecting a custom icon therefore creates a second app-drawer icon.
This changes the launcher architecture so MainActivity remains enabled for notifications, sharing, deep links, media intents, shortcuts, and explicit launches, while every launcher icon, including the default icon, is represented by an activity alias. Exactly one alias is enabled at a time.
The change also:
Fixes #1252
Testing Instructions
Automated checks completed:
Note
One UI launcher cache: after switching icons, Samsung's launcher may keep showing app-drawer entries for previously enabled aliases for a while, even though PackageManager reports exactly one enabled launcher component (
adb shell cmd package query-activities -a android.intent.action.MAIN -c android.intent.category.LAUNCHER --brief | grep pocketcasts). These stale entries disappear once the launcher refreshes (restarting the launcher or rebooting forces it). Testers should use thequery-activitiesoutput as ground truth rather than the drawer, so launcher cache isn't mistaken for a regression.Verified on a Galaxy S21 FE (Android 16 / One UI 8): duplicate icons reproduced on the pre-fix build, then a dirty upgrade to this branch migrated to exactly one enabled alias (custom selection preserved), with notification launches, launch-intent resolution, static shortcuts, and icon switching all confirmed working. Android Auto is unaffected: it surfaces the app via the
MediaBrowserServicedeclarations andcom.google.android.gms.car.applicationmeta-data, which this change doesn't touch.Screenshots or Screencast
Captured on a Galaxy S21 FE (Android 16 / One UI 8), same app-drawer page, debug build. Before: pre-fix build with the Dark icon selected shows two entries (default + Dark). After: dirty upgrade to this branch, the migration receiver leaves only the selected Dark icon.
Before (pre-fix build, Dark icon selected, two entries):
After (this branch installed over it, one entry, selection preserved):
Checklist
./gradlew spotlessApplyto automatically apply formatting/linting)AppIconTestcovering the atomic API 33+ switch, the pre-13 enable-before-disable ordering, and the default-icon casemodules/services/localization/src/main/res/values/strings.xml— the reworded dialog message uses a new key (settings_app_icon_updated_message_v2) so stale translations of the old copy don't shadow itSettingsAppearanceAppIconChangedEventis untouched)I have tested any UI changes...
n/a — this PR changes launcher/alias behavior and one dialog message; there are no layout or screen changes. Launcher behavior was verified on-device (Galaxy S21 FE, One UI 8): duplicate reproduced pre-fix, dirty upgrade migrated to a single entry with the user's selection preserved, and notification launches, launch-intent resolution, static shortcuts, and icon switching all confirmed working.