需求背景
大家可能会有注意到,每逢重大节日,很多应用图标会自动调整,类似于春节版、国庆版等等。
这个功能最简单的实现方式可能就是发布一个新的版本了,直接替换相关资源,然后应用升级体验。 但是这种方式工作量较大,很不方便。并且像今日头条、支付宝这类软件,我们好像也没有注意到有应用升级就实现了图标替换,很神奇吧,今天我们就实现这个功能。
实现过程
以开源项目睡眠助手为例,实现应用切换图标功能。 首先我们找到清单文件AndroidManifest.xml,可以看到启动Activity配置如下:
<activity
android:exported="true"
android:name=".activity.GuideActivity"
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
然后我们在该activity定义之后,添加新的定义文件,定义一个activity-alias。 需注意,该activity-alias一定要在启动activity之后定义才可。
<activity
android:exported="true"
android:name=".activity.GuideActivity"
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:exported="true"
android:icon="@mipmap/icon"
android:label="睡眠猪猪"
android:name=".activity.NewGuideActivity"
android:targetActivity=".activity.GuideActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
此时安装应用,我们会发现,桌面上会出现两个应用:睡眠助理、睡眠猪猪,点击两个图标均可实现打开应用,使用功能。那么很显然activity-alias实现了新的应用入口。我们要实现应用图标变更,那么可以先把activity-alias状态关闭,需要开启时再进行开启,通过android:enabled="false"进行设置:
<activity
android:exported="true"
android:name=".activity.GuideActivity"
android:theme="@style/AppTheme.Launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity-alias
android:enabled="false"
android:exported="true"
android:icon="@mipmap/icon"
android:label="睡眠猪猪"
android:name=".activity.NewGuideActivity"
android:targetActivity=".activity.GuideActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
现在启动应用会看到图标又恢复成一个了,接下来实现控制图标的变更。
动态控制应用图标可以使用PackageManager实现,可以借助于推送、时间判断、用户点击等方式触发,我们演示功能就采用用户点击的方式。在设置界面添加操作按钮,实现点击进行变更:
PackageManager pm = getPackageManager();
if(PackageManager.COMPONENT_ENABLED_STATE_DISABLED != pm.getComponentEnabledSetting(new ComponentName(this, "com.devdroid.sleepassistant.activity.GuideActivity"))) {
pm.setComponentEnabledSetting(new ComponentName(this, "com.devdroid.sleepassistant.activity.GuideActivity"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(new ComponentName(this, "com.devdroid.sleepassistant.activity.NewGuideActivity"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
} else {
pm.setComponentEnabledSetting(new ComponentName(this, "com.devdroid.sleepassistant.activity.NewGuideActivity"),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(new ComponentName(this, "com.devdroid.sleepassistant.activity.GuideActivity"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
此时我们就通过按钮实现图标的切换功能了。
发现问题
- 问题一
由小伙伴反馈,一旦切换图标后,应用安装会出现问题:
Error while executing: am start -n "com.devdroid.sleepassistant/com.devdroid.sleepassistant.activity.GuideActivity" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.devdroid.sleepassistant/.activity.GuideActivity }
Error type 3
Error: Activity class {com.devdroid.sleepassistant/com.devdroid.sleepassistant.activity.GuideActivity} does not exist.
Error while Launching activity
仔细看提示,发现并不是应用安装出现问题。之所以报这个错误,是因为该小伙伴直接从Android Studio运行应用。由于默认的启动Activity已经被设置为COMPONENT_ENABLED_STATE_DISABLED(不可用),所以i同无法找到默认的Activity,无法启动应用,报错了。若是用户使用安装包或从应用商店安装则不存在该问题。
- 问题二
有小伙伴反馈变更应用图标后,应用会再3秒后关闭。 我们看一下变更图标的方法:setComponentEnabledSetting():
Set the enabled setting for a package component (activity, receiver, service, provider). This setting will override any enabled state which may have been set by the component in its manifest.
翻译为:
设置包四大组件(activity, receiver, service, provider)的启用设置。此设置将覆盖组件在其清单文件(AndroidManifest.xml)中设置的任何启用状态。
其中有一个flags参数,可选为:DONT_KILL_APP,SYNCHRONOUS。其中: DONT_KILL_APP
Flag parameter for setComponentEnabledSetting(ComponentName, int, int) to indicate that you don't want to kill the app containing the component. Be careful when you set this since changing component states can make the containing application's behavior unpredictable.
翻译:
setComponentEnabledSetting(ComponentName,int,int)的标志参数,用于指示您不希望终止包含该组件的应用程序。设置此选项时要小心,因为更改组件状态会使包含应用程序的行为不可预测。
SYNCHRONOUS
Flag parameter for setComponentEnabledSetting(ComponentName, int, int) to indicate that the given user's package restrictions state will be serialised to disk after the component state has been updated. Note that this is synchronous disk access, so calls using this flag should be run on a background thread.
翻译:
setComponentEnabledSetting(ComponentName,int,int)的标志参数,用于指示给定用户的包限制状态将在更新组件状态后序列化到磁盘。请注意,这是同步磁盘访问,因此使用此标志的调用应该在后台线程上运行。
测试发现:使用DONT_KILL_APP时,应用在3秒内退出;使用SYNCHRONOUS应用立即退出。
但是DONT_KILL_APP和实际不符啊,为什么呢?
网上查阅资料,大多回答是一头雾水,有人反馈是Android系统的一个系统级bug。自己到谷歌社区查找主题,通过官方人员沟通,了解到:当使用DONT_KILL_APP时,Application不会主动结束进程,但是由于作为启动页的GuideActivity被设置为COMPONENT_ENABLED_STATE_DISABLED(不可用),这时候APP会将GuideActivity创建的任务栈清空,由于APP所有Activity都是由GuideActivity任务栈创建的,所以就看到类似于退出应用的效果。 好了,原因确定了,那么就看如何解决了。
此时我们只需要使用新的任务栈启动SettingsActivity,然后在SettingsActivity内清空启动栈,应用就不会退出了。
Intent intent = new Intent(mAppCompatActivity, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
mAppCompatActivity.startActivity(intent);
注意: 实际使用时,发现setComponentEnabledSetting生效速度较慢,大概有3s左右。在3s内启动应用,会仍然调用原来的启动页面,导致3s退出应用时,将新建的任务栈清空,应用退出。
原因了解了,通过代码验证,确实可以借助上面的方式实现图标变更。
但是该方案还存在一个弊端:当GuideActivity设置不可用时,应用内其他页面需要跳转到GuideActivity时是不能实现的,同时也无法跳转到activity-alias定义的NewGuideActivity中,这个暂时没有找到解决方案。
通过谷歌官方人员沟通,了解到官方不建议通过使用activity-alias方式实现这种功能,他们提供了一种新的方案。
最终方案
谷歌认为,图标变更功能应该使用独立的LAUNCHER Activity实现,而不应借助activity-alias。
最建议方案如下:
首先创建类文件NewGuideActivity,实现如下代码:
class NewGuideActivity extends GuideActivity{
}
清单文件添加声明:
<activity
android:enabled="false"
android:exported="true"
android:icon="@mipmap/icon"
android:label="睡眠猪猪"
android:name=".activity.NewGuideActivity"
android:targetActivity=".activity.GuideActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
这时候,NewGuideActivity就是一个真实的LAUNCHER了。由于NewGuideActivity直接继承GuideActivity,本身没有任何实质代码,所以功能也是完全一致的。对于NewGuideActivity、GuideActivity的设置和activity-alias方式类似。这个能够满足变更的需求。
以上相关代码请参考开源项目:睡眠助手