Implement dynamic application icons in Android

Implement dynamic application icons in Android

You’ve probably come across those apps that pull off a magical trick – change their app icon on your birthday, then seamlessly switch back to the regular icon. It’s the kind of feature that sparks your curiosity and makes you wonder, “How on earth do they do that?”. Well, you’re not alone in this curiosity. Many developers, including myself, have also thought about this problem. It may seem like a seemingly impossible task, but guess what? It is not! In this article, we will unravel the mystery of changing Android app icons at runtime. We’ll walk through it step by step and show you that not only is it doable, but it’s also fairly easy to manage.

First, the app icon is set from a manifest file similar to other app components. The Android system reads the manifest file and sets the app icon accordingly. There is currently no way to change the app icon at runtime. But there is a workaround. That is to use activity-alias (if you are not familiar with activity-alias, you can check it out in the official documentation).

https://developer.android.com/guide/topics/manifest/activity-alias-element

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        ...
        android:icon="YOUR_ICON"
        android:roundIcon="YOUR_ICON"
    >
        <activity
            ...
            android:name=".MainActivity"
        >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity-alias
            ...
            android:icon="YOUR_ICON_2"
            android:roundIcon="YOUR_ICON_2"
            android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>
    </application>
</manifest>

As you can see, we have two activities. One is the main activity and the other is the activity alias. Activity aliases are disabled by default and have a different icon than the main activity. So when the application is installed, the icon of the main activity will be set. And when we enable active aliases, the icon for the active alias will be set. Therefore, we can change the application icon at runtime by enabling and disabling active aliases. Now, let’s see how to enable and disable active aliases at runtime. We can achieve this using the PackageManager class.

fun Activity.changeIcon() {<!-- -->
        packageManager.setComponentEnabledSetting(
            ComponentName(
                this,
                "$packageName.MainActivityAlias"
            ),
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
            PackageManager.DONT_KILL_APP
        )

        packageManager.setComponentEnabledSetting(
            ComponentName(
                this,
                "$packageName.MainActivity"
            ),
            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
            PackageManager.DONT_KILL_APP
        )
}

As you can see, we are using the setComponentEnabledSetting method of the PackageManager class. We are passing the activity alias and the component name of the main activity and setting the activity alias to enabled state and the main activity to disabled state. So when we call this method the activity alias will be enabled and the main activity will be disabled. Therefore, the application icon will be changed.

As a software engineer, I don’t like this implementation. I’d rather things be simple and flexible. Therefore, I thought it would be better to update the above function as follows:

fun Activity.changeEnabledComponent(
        enabled: String,
        disabled: String,
    ) {<!-- -->
        packageManager.setComponentEnabledSetting(
            ComponentName(
                this,
                enabled
            ),
            PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
            PackageManager.DONT_KILL_APP
        )

    packageManager.setComponentEnabledSetting(
            ComponentName(
                this,
                disabled
            ),
            PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
            PackageManager.DONT_KILL_APP
    )
}

Therefore, changing the application icon will only require calling the function with the component name. For example:

changeEnabledComponent(
    enabled = "$packageName.MainActivityAlias",
    disabled = "$packageName.MainActivity"
)

As a software engineer, I’m not happy with the fact that we’re still using hardcoding. I even wish things were more flexible and easier to change. So I want to be more abstract with the component names. But there is a challenge here as we have to somehow get the same name as used in the manifest file. To solve this problem, we can use BuildConfig and manifestPlaceholders instead of using hard-coded strings. In the build.gradle file at the application level, we can add the following code:

 private val mainActivity = "YOURPATH.MainActivity"
    private val mainActivityAlias = "YOURPATH.MainActivityAlias"
    android {<!-- -->
        defaultConfig {<!-- -->
            ...
            manifestPlaceholders.apply {<!-- -->
                set("main_activity", mainActivity)
                set("main_activity_alias", mainActivityAlias)
            }
        }

        buildTypes {<!-- -->
            release {<!-- -->
                isMinifyEnabled = false
                buildConfigField("String", "main_activity", ""${<!-- -->mainActivity}"")
                buildConfigField("String", "main_activity_alias", ""${<!-- -->mainActivityAlias}"")
            }

            debug {<!-- -->
                isDebuggable = true
                isMinifyEnabled = false
                buildConfigField("String", "main_activity", ""${<!-- -->mainActivity}"")
                buildConfigField("String", "main_activity_alias", ""${<!-- -->mainActivityAlias}"")
            }
        }
    }

Here we set the component names to manifestPlaceholders and buildConfigField. Therefore, we can access them from the BuildConfig class. Of course, we need to update the manifest file to use these placeholders.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        ...
    >
        <activity
            android:name="${main_activity}"
            ...
        >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity-alias
            android:name="${main_activity_alias}"
            ...
            android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>
    </application>
</manifest>

You may see some errors stating that main_activity or main_activity_alias cannot be found. But you can ignore them as they will be generated synchronously with the build.gradle file. Now, we can update our code to use the BuildConfig class.

changeEnabledComponent(
    enabled = BuildConfig.main_activity_alias,
    disabled = BuildConfig.main_activity
)

Now we have a clean and flexible code. We can change the application icon at runtime by calling the changeEnabledComponent function and passing the component name. We can also change the component name in the build.gradle file. Therefore, we can change the application icon at runtime without changing the code. If you need a more detailed example, check out the code snippet below.

val mainActivity = BuildConfig.main_activity
val mainActivityAlias = BuildConfig.main_activity_alias

class MainActivity : ComponentActivity() {<!-- -->
    override fun onCreate(savedInstanceState: Bundle?) {<!-- -->
        super.onCreate(savedInstanceState)

        setContent {<!-- -->
            DynamicIconTheme {<!-- -->
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {<!-- -->
                    Screen(
                        on30Click = {<!-- -->
                            changeEnabledComponent(
                                enabled = mainActivityAlias,
                                disabled = mainActivity
                            )
                        },
                        on60Click = {<!-- -->
                            changeEnabledComponent(
                                enabled = mainActivityAlias,
                                disabled = mainActivity
                            )
                        }
                    )
                }
            }
        }
    }
}

Conclusion

Essentially, we dispel the myth that dynamically changing Android app icons at runtime is unachievable. By leveraging the power of activity-alias in the application manifest and expertly using the PackageManager class, we reveal how to achieve this goal. However, the real change is in our pursuit of cleaner, more adaptable code, where we leverage placeholders and BuildConfig to achieve extreme flexibility. Now you can let users inject their own unique style into your app icons without getting lost in a morass of code.

Github

https://github.com/oguzhanaslann/DynamicIcon

Reference

https://www.geeksforgeeks.org/how-to-change-app-icon-of-android-programmatically-in-android/
https://developer.android.com/guide/topics/manifest/activity-alias-element