Source code
Revision control
Copy as Markdown
Other Tools
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
package org.mozilla.fenix.utils
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.nimbus.FxNimbus
private val logger = Logger("ChangeAppLauncherIcon")
/**
* Required to implement the [FxNimbus.features] alternativeAppLauncherIcon feature.
*
* Will set the app launcher icon based on the [resetToDefault] flag.
* Checks whether the icon needs updating to prevent unnecessary icon updates.
*
* @param context The application [Context].
* @param shortcutManager [ShortcutManagerWrapper] to safely access some features in [ShortcutInfoCompat].
* @param shortcutInfo A helper class for updating [ShortcutInfoCompat].
* @param appAlias The 'default' app alias defined in AndroidManifest.xml.
* @param alternativeAppAlias The 'alternative' app alias defined in AndroidManifest.xml.
* @param resetToDefault True to reset the icon to default, otherwise false to use the alternative icon.
* @param crashReporter An instance of [CrashReporting] to record expected caught exceptions during
* shortcuts update.
*/
fun changeAppLauncherIcon(
context: Context,
shortcutManager: ShortcutManagerWrapper = ShortcutManagerWrapperDefault(context),
shortcutInfo: ShortcutsUpdater = ShortcutsUpdaterDefault(context),
appAlias: ComponentName,
alternativeAppAlias: ComponentName,
resetToDefault: Boolean,
crashReporter: CrashReporting,
) {
val userHasAlternativeAppIconSet =
userHasAlternativeAppIconSet(context.packageManager, appAlias, alternativeAppAlias)
if (resetToDefault && userHasAlternativeAppIconSet) {
resetAppIconsToDefault(context, shortcutManager, shortcutInfo, appAlias, alternativeAppAlias, crashReporter)
return
}
if (!resetToDefault && !userHasAlternativeAppIconSet) {
changeAppLauncherIcon(
context.packageManager,
shortcutManager,
shortcutInfo,
appAlias,
alternativeAppAlias,
crashReporter,
)
}
}
private fun userHasAlternativeAppIconSet(
packageManager: PackageManager,
appAlias: ComponentName,
alternativeAppAlias: ComponentName,
): Boolean {
val appAliasState = packageManager.getComponentEnabledSetting(appAlias)
val alternativeAppAliasState =
packageManager.getComponentEnabledSetting(alternativeAppAlias)
// If the default App alias was explicitly disabled AND the experiment AppAlternative alias
// has been explicitly enabled, then the user already has the alternative app launcher icon set.
return appAliasState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED &&
alternativeAppAliasState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
/**
* Updates the app launcher icon with the provided alternative alias.
*
* @param packageManager responsible for enabled/disabled state of the aliases.
* @param shortcutManager A wrapper for [ShortcutInfoCompat].
* @param shortcutInfo A helper class for updating [ShortcutInfoCompat].
* @param appAlias The currently used app alias.
* @param newAppAlias The app alias we are updating to.
* @param crashReporter An instance of [CrashReporting] to record expected caught exceptions during
* shortcuts update.
* @param updateShortcuts A function that attempts to update the pinned shortcuts to use the [newAppAlias].
*
* @returns `true` if the app icon was successfully updated, otherwise `false`
*/
fun changeAppLauncherIcon(
packageManager: PackageManager,
shortcutManager: ShortcutManagerWrapper,
shortcutInfo: ShortcutsUpdater,
appAlias: ComponentName,
newAppAlias: ComponentName,
crashReporter: CrashReporting,
updateShortcuts: (ShortcutManagerWrapper, ShortcutsUpdater, ComponentName, CrashReporting) -> Boolean =
::updateShortcutsComponentName,
): Boolean {
newAppAlias.setEnabledStateTo(packageManager, true)
val updated = updateShortcuts(shortcutManager, shortcutInfo, newAppAlias, crashReporter)
if (updated) {
logger.info("Successfully attempted to update the app icon to the alternative.")
appAlias.setEnabledStateTo(packageManager, false)
} else {
logger.warn("Failed to attempt to update the app icon.")
// If we can't successfully update the user shortcuts, then re-disable the app alias component.
newAppAlias.setEnabledStateTo(packageManager, false)
}
return updated
}
private fun resetAppIconsToDefault(
context: Context,
shortcutManager: ShortcutManagerWrapper,
shortcutInfo: ShortcutsUpdater,
appAlias: ComponentName,
alternativeAppAlias: ComponentName,
crashReporter: CrashReporting,
) {
val packageManager = context.packageManager
appAlias.setEnabledStateToDefault(packageManager)
if (updateShortcutsComponentName(shortcutManager, shortcutInfo, appAlias, crashReporter)) {
logger.info("Successfully attempted to reset the app icon to default.")
alternativeAppAlias.setEnabledStateToDefault(packageManager)
} else {
logger.warn("Failed to attempt to reset the app icon.")
// If we can't successfully update the user shortcuts, then re-disable the app alias component.
appAlias.setEnabledStateTo(packageManager, false)
}
}
private fun ComponentName.setEnabledStateToDefault(packageManager: PackageManager) {
packageManager.setComponentEnabledSetting(
this,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
PackageManager.DONT_KILL_APP,
)
}
private fun ComponentName.setEnabledStateTo(packageManager: PackageManager, enabled: Boolean) {
val newState = if (enabled) {
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
} else {
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
}
packageManager.setComponentEnabledSetting(
this,
newState,
PackageManager.DONT_KILL_APP,
)
}
/**
* Attempt to update the device's existing shortcuts to the given [targetAlias] state.
*
* @return whether updating the shortcuts to the app alias was successful.
*/
@VisibleForTesting
internal fun updateShortcutsComponentName(
shortcutManager: ShortcutManagerWrapper,
shortcutInfo: ShortcutsUpdater,
targetAlias: ComponentName,
crashReporter: CrashReporting,
): Boolean {
val currentPinnedShortcuts = try {
shortcutManager.getPinnedShortcuts()
} catch (e: IllegalStateException) {
crashReporter.submitCaughtException(e)
logger.warn("Failed to retrieve the current Firefox pinned shortcuts", e)
return false
}
val updatedPinnedShortcuts = shortcutInfo.buildUpdatedShortcuts(currentPinnedShortcuts, targetAlias)
return try {
shortcutManager.updateShortcuts(updatedPinnedShortcuts)
true
} catch (e: IllegalArgumentException) {
crashReporter.submitCaughtException(e)
logger.warn("Failed to update the given Firefox shortcuts: $updatedPinnedShortcuts", e)
false
}
}
/**
* Wrapper to safely access some features in [ShortcutManagerCompat].
*/
interface ShortcutManagerWrapper {
/**
* @return a list of the current pinned shortcuts for Firefox.
*
* @throws IllegalStateException when the user is locked
*/
fun getPinnedShortcuts(): List<ShortcutInfoCompat>
/**
* Update all existing Firefox shortcuts to the given [updatedShortcuts].
*
* @throws IllegalArgumentException if trying to update immutable shortcuts.
*/
fun updateShortcuts(updatedShortcuts: List<ShortcutInfoCompat>)
}
/**
* Implementation of [ShortcutManagerWrapper] that uses [ShortcutManagerCompat].
*/
class ShortcutManagerWrapperDefault(private val context: Context) : ShortcutManagerWrapper {
override fun getPinnedShortcuts(): List<ShortcutInfoCompat> =
ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_PINNED)
override fun updateShortcuts(updatedShortcuts: List<ShortcutInfoCompat>) {
ShortcutManagerCompat.updateShortcuts(context, updatedShortcuts)
}
}
/**
* A helper class for updating [ShortcutInfoCompat].
*/
interface ShortcutsUpdater {
/**
* @return a list of shortcuts for Firefox updated with the provided alternative alias.
*/
fun buildUpdatedShortcuts(
shortcuts: List<ShortcutInfoCompat>,
alternativeAppAlias: ComponentName,
): List<ShortcutInfoCompat>
}
/**
* A default implementation of [ShortcutsUpdater].
*/
class ShortcutsUpdaterDefault(
private val context: Context,
) : ShortcutsUpdater {
override fun buildUpdatedShortcuts(
shortcuts: List<ShortcutInfoCompat>,
alternativeAppAlias: ComponentName,
): List<ShortcutInfoCompat> {
return shortcuts.map { updateShortcutComponentName(it, alternativeAppAlias) }
}
private fun updateShortcutComponentName(
originalShortcut: ShortcutInfoCompat,
alternativeAppAlias: ComponentName,
): ShortcutInfoCompat {
with(originalShortcut) {
val builder = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setIntent(intent)
.setActivity(alternativeAppAlias) // this links the shortcut to the new component name.
longLabel?.let { builder.setLongLabel(it) }
return builder.build()
}
}
}