You are reading an old article on an old site. It only still exists so the information is not lost. Comments and mosts links are hidden and disabled.

[TUT] Supporting multiple icons in your app

After a lot of noise about the icon to use for SuperSU, I finally released SuperSU v1.00 in late January 2013, with support for multiple icons that the user could pick from.

As far as I know, SuperSU was the first app on Android to allow you to choose between several different icons. Other developers quickly picked up on this, and I have received quite a few requests to explain how I did it.

It is in fact very easy to do, and once you've seen how it's done, you'll probably think "heck, I could've thought of that !". And you could have - within a day of the SuperSU release that brought this feature, Ronald Ammann already figured it out after having a quick glance at SuperSU's AndroidManifest.xml file.

Long story short: use <activity-alias> and PackageManager.setComponentEnabledSetting().

Short story long: following is how I thought of it, the nitty gritty, and copy/pastes straight from SuperSU sources.

Instead of using only the winning icon from the competition, I wanted to give the end-user the choice of several options. Thinking for a few minutes how to accomplish this, the first thing that came to mind was to use multiple launcher activities (you can have as many activities listed in launcher as you want) that simply launched the main activity in their onCreate's and passed it some extra parameter in the intent, so the main activity knew which icon to show in the ActionBar, and finish(). I quickly realised this was a terrible idea and ain't nobody got time for that to make all these copy/paste activities.

Then I remembered seeing <activity-alias> somewhere in the docs. I had never actually had a use for it, so I looked it up to see if it could be the solution I was looking for - and indeed it was. By using <activity-alias> I could easily create several icon options leading to the same activity.

But of course we do not want all the icons to show in the launcher, so we need a way to select which one to show. We can do this easily in the AndroidManifest.xml file using the android:enabled attribute of the <activity-alias> tag to define the default, and use PackageManager.setComponentEnabledSetting() in code to switch to a different icon.

The Android Manifest

Here's a relevant excerpt from SuperSU's AndroidManifest.xml file (some lengthy and irrelevant attributes to this example stripped), with two of the five <activity-alias>'s:

Code
#
1
<activity
2
    android:name=".MainActivity"
3
    android:label="@string/app_name"
4
    ... >
5
    <intent-filter>
6
        <action android:name="android.intent.action.MAIN" />
7
    </intent-filter>
8
</activity>
9
    
10
<activity-alias
11
    android:enabled="true"
12
    android:name=".MainActivity-SuperAndy"
13
    android:label="@string/app_name"
14
    android:icon="@drawable/ic_launcher_superandy"
15
    android:targetActivity=".MainActivity">
16
    <intent-filter>
17
        <action android:name="android.intent.action.MAIN" />
18
        <category android:name="android.intent.category.LAUNCHER" />
19
    </intent-filter>
20
</activity-alias> 
21
 
22
<activity-alias
23
    android:enabled="false"
24
    android:name=".MainActivity-Original"
25
    android:label="@string/app_name"
26
    android:icon="@drawable/ic_launcher_original"
27
    android:targetActivity=".MainActivity">
28
    <intent-filter>
29
        <action android:name="android.intent.action.MAIN" />
30
        <category android:name="android.intent.category.LAUNCHER" />
31
    </intent-filter>
32
</activity-alias>
33
 
34
...

As you can see, the <activity-alias>'s point back to .MainActivity - which itself does not show up in launcher, as it does not have the launcher category set. .MainActivity-SuperAndy is the one shown by default, it's the only with android:enabled set to true.

CAVEAT: If you duplicate this, make sure that your default alias - the only alias which is enabled in the XML - is also the first <activity-alias> listed. I'm not sure what exactly the cause is, but I've had issues with no icon showing up at all on some Android versions if the enabled alias was not also the first one.

CAVEAT: (Eclipse) If you try to run the app now, it is likely that Eclipse/ADT/whatever-is-responsible will not be able to deduce how to launch your app, so it will just install your app, instead of launching it. To work around this, go to your project's properties, Run/Debug Settings, Edit your launch configuration, and select your main activity to launch manually.

The Java Code

Here's a slightly modified excerpt of the code in SuperSU to actually change the icon:

Code
#
1
private void setIcon(int icon) {
2
    Context ctx = getActivity();
3
    PackageManager pm = getActivity().getPackageManager();
4
    ActivityManager am = getActivity().getSystemService(Activity.ACTIVITY_SERVICE);
5
    
6
    // Enable/disable activity-aliases
7
    
8
    pm.setComponentEnabledSetting(
9
            new ComponentName(ctx, "eu.chainfire.supersu.MainActivity-Original"), 
10
            icon == Constants.ICON_ORIGINAL ? 
11
                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED : 
12
                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
13
            PackageManager.DONT_KILL_APP
14
    );
15
 
16
    pm.setComponentEnabledSetting(
17
            new ComponentName(ctx, "eu.chainfire.supersu.MainActivity-SuperAndy"), 
18
            icon == Constants.ICON_SUPERANDY ? 
19
                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED : 
20
                        PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 
21
            PackageManager.DONT_KILL_APP
22
    );
23
 
24
    ...
25
 
26
    // Find launcher and kill it
27
    
28
    Intent i = new Intent(Intent.ACTION_MAIN);
29
    i.addCategory(Intent.CATEGORY_HOME);
30
    i.addCategory(Intent.CATEGORY_DEFAULT);
31
    List<ResolveInfo> resolves = pm.queryIntentActivities(i, 0);
32
    for (ResolveInfo res : resolves) {
33
        if (res.activityInfo != null) {
34
            am.killBackgroundProcesses(res.activityInfo.packageName);
35
        }
36
    }
37
    
38
    // Change ActionBar icon
39
    
40
    getActivity().getActionBar().setIcon(getIconResId(icon));        
41
}    

This piece of code first enables/disables the relevant <activity-alias>'s based on the icon value passed in. This doesn't actually update the icon in the launcher - for that the launcher needs to be restarted. So the next thing the code does is find the launcher and attempt to kill it. Finally, it will change the icon in the ActionBar. We don't need to set the ActionBar icon when our activity starts, as the ActionBar is smart enough to show the icon associated with the <activity-alias> used to start our activity.

There are a few caveats and things of note in this small piece of code:

- This code is from a Fragment, hence the getActivity() calls. In a normal activity you would just use this instead.

- The icon value passed is arbitrary. It is defined elsewhere in a Constants class, as are the ICON_ORIGINAL and ICON_SUPERANDY values. You get the idea.

- The <activity-alias> names are hardcoded strings (bah!), there doesn't seem to be a way to reference them so the compiler will catch the problem if any relevant names change (accident waiting to happen).

- ActivityManager.killBackgroundProcesses is used in this example, which requires the android.permission.KILL_BACKGROUND_PROCESSES permission. You are not guaranteed to actually get this permission, nor is it guaranteed it will actually kill the launcher (as it is not necessarily a background process). This is not a problem for SuperSU, as it does not actually use this call but issues a "kill" command as root. Obviously your app will not be able to do that (unless it targets root users), so you may need to thoroughly test under which conditions the ActivityManager.killBackgroundProcesses does and does not work, and you might want to show the end-user a message that the icon change may not take effect until the next device reboot.

- If you're using ActionBarSherlock, you should be using getSupportActionBar() instead of getActionBar() to change the icon. If you're testing on a device that natively supports the ActionBar, the getActionBar() call will actually work as expected, but it will crash your app once you run it on a 2.x device. While this should be obvious, it wasn't so obvious that I didn't do it wrong myself - luckily I caught it in testing.

Conclusion

This is all there is to it - it is as easy as it looks. It is claimed in several places it is not possible to change your launcher icon, I've shown you that it certainly is possible, and it isn't even complicated. It took me less time to think it up, code, and test it, than it took me to write this article. It is unfortunate however that this method does not allow you to use any arbitrary icon - that would have been even neater. But you can't win them all (... yet).

This article also doesn't touch on the matter if you should offer the user the option to change the icon - which is a topic for lengthy debate I'm not personally interested in. Feel free to beat up your local philosopher until he gives you an answer you can agree with.

I had actually planned to include how to switch themes and override locales at runtime (features SuperSU also offers), but I will leave those for a different article.

Enjoy!