需求背景
大家可能會(huì)有注意到睡汹,每逢重大節(jié)日肴甸,很多應(yīng)用圖標(biāo)會(huì)自動(dòng)調(diào)整,類(lèi)似于春節(jié)版帮孔、國(guó)慶版等等雷滋。
這個(gè)功能最簡(jiǎn)單的實(shí)現(xiàn)方式可能就是發(fā)布一個(gè)新的版本了不撑,直接替換相關(guān)資源文兢,然后應(yīng)用升級(jí)體驗(yàn)匆骗。 但是這種方式工作量較大官紫,很不方便利诺。并且像今日頭條餐济、支付寶這類(lèi)軟件积暖,我們好像也沒(méi)有注意到有應(yīng)用升級(jí)就實(shí)現(xiàn)了圖標(biāo)替換噪径,很神奇吧满败,今天我們就實(shí)現(xiàn)這個(gè)功能驮瞧。
實(shí)現(xiàn)過(guò)程
以開(kāi)源項(xiàng)目睡眠助手為例腊敲,實(shí)現(xiàn)應(yīng)用切換圖標(biāo)功能击喂。 首先我們找到清單文件AndroidManifest.xml,可以看到啟動(dòng)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>
然后我們?cè)谠揳ctivity定義之后碰辅,添加新的定義文件懂昂,定義一個(gè)activity-alias。 需注意没宾,該activity-alias一定要在啟動(dòng)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>
此時(shí)安裝應(yīng)用,我們會(huì)發(fā)現(xiàn)循衰,桌面上會(huì)出現(xiàn)兩個(gè)應(yīng)用:睡眠助理铲敛、睡眠豬豬,點(diǎn)擊兩個(gè)圖標(biāo)均可實(shí)現(xiàn)打開(kāi)應(yīng)用会钝,使用功能伐蒋。那么很顯然activity-alias實(shí)現(xiàn)了新的應(yīng)用入口。我們要實(shí)現(xiàn)應(yīng)用圖標(biāo)變更迁酸,那么可以先把a(bǔ)ctivity-alias狀態(tài)關(guān)閉咽弦,需要開(kāi)啟時(shí)再進(jìn)行開(kāi)啟,通過(guò)android:enabled="false"進(jìn)行設(shè)置:
<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>
現(xiàn)在啟動(dòng)應(yīng)用會(huì)看到圖標(biāo)又恢復(fù)成一個(gè)了胁出,接下來(lái)實(shí)現(xiàn)控制圖標(biāo)的變更型型。
動(dòng)態(tài)控制應(yīng)用圖標(biāo)可以使用PackageManager實(shí)現(xiàn),可以借助于推送全蝶、時(shí)間判斷闹蒜、用戶(hù)點(diǎn)擊等方式觸發(fā)寺枉,我們演示功能就采用用戶(hù)點(diǎn)擊的方式。在設(shè)置界面添加操作按鈕绷落,實(shí)現(xiàn)點(diǎn)擊進(jìn)行變更:
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);
}
此時(shí)我們就通過(guò)按鈕實(shí)現(xiàn)圖標(biāo)的切換功能了姥闪。
發(fā)現(xiàn)問(wèn)題
- 問(wèn)題一
由小伙伴反饋,一旦切換圖標(biāo)后砌烁,應(yīng)用安裝會(huì)出現(xiàn)問(wèn)題:
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
仔細(xì)看提示筐喳,發(fā)現(xiàn)并不是應(yīng)用安裝出現(xiàn)問(wèn)題。之所以報(bào)這個(gè)錯(cuò)誤函喉,是因?yàn)樵撔』锇橹苯訌腁ndroid Studio運(yùn)行應(yīng)用避归。由于默認(rèn)的啟動(dòng)Activity已經(jīng)被設(shè)置為COMPONENT_ENABLED_STATE_DISABLED(不可用),所以i同無(wú)法找到默認(rèn)的Activity管呵,無(wú)法啟動(dòng)應(yīng)用梳毙,報(bào)錯(cuò)了。若是用戶(hù)使用安裝包或從應(yīng)用商店安裝則不存在該問(wèn)題捐下。
- 問(wèn)題二
有小伙伴反饋?zhàn)兏鼞?yīng)用圖標(biāo)后账锹,應(yīng)用會(huì)再3秒后關(guān)閉。 我們看一下變更圖標(biāo)的方法: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.
翻譯為:
設(shè)置包四大組件(activity, receiver, service, provider)的啟用設(shè)置坷襟。此設(shè)置將覆蓋組件在其清單文件(AndroidManifest.xml)中設(shè)置的任何啟用狀態(tài)奸柬。
其中有一個(gè)flags參數(shù),可選為: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)的標(biāo)志參數(shù)排抬,用于指示您不希望終止包含該組件的應(yīng)用程序懂从。設(shè)置此選項(xiàng)時(shí)要小心,因?yàn)楦慕M件狀態(tài)會(huì)使包含應(yīng)用程序的行為不可預(yù)測(cè)蹲蒲。
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)的標(biāo)志參數(shù)届搁,用于指示給定用戶(hù)的包限制狀態(tài)將在更新組件狀態(tài)后序列化到磁盤(pán)缘薛。請(qǐng)注意,這是同步磁盤(pán)訪問(wèn)卡睦,因此使用此標(biāo)志的調(diào)用應(yīng)該在后臺(tái)線(xiàn)程上運(yùn)行宴胧。
測(cè)試發(fā)現(xiàn):使用DONT_KILL_APP時(shí),應(yīng)用在3秒內(nèi)退出表锻;使用SYNCHRONOUS應(yīng)用立即退出恕齐。
但是DONT_KILL_APP和實(shí)際不符啊,為什么呢瞬逊?
網(wǎng)上查閱資料显歧,大多回答是一頭霧水仪或,有人反饋是Android系統(tǒng)的一個(gè)系統(tǒng)級(jí)bug。自己到谷歌社區(qū)查找主題士骤,通過(guò)官方人員溝通范删,了解到:當(dāng)使用DONT_KILL_APP時(shí),Application不會(huì)主動(dòng)結(jié)束進(jìn)程拷肌,但是由于作為啟動(dòng)頁(yè)的GuideActivity被設(shè)置為COMPONENT_ENABLED_STATE_DISABLED(不可用)到旦,這時(shí)候APP會(huì)將GuideActivity創(chuàng)建的任務(wù)棧清空,由于APP所有Activity都是由GuideActivity任務(wù)棧創(chuàng)建的巨缘,所以就看到類(lèi)似于退出應(yīng)用的效果添忘。 好了,原因確定了带猴,那么就看如何解決了昔汉。
此時(shí)我們只需要使用新的任務(wù)棧啟動(dòng)SettingsActivity懈万,然后在SettingsActivity內(nèi)清空啟動(dòng)棧拴清,應(yīng)用就不會(huì)退出了。
Intent intent = new Intent(mAppCompatActivity, SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
mAppCompatActivity.startActivity(intent);
注意: 實(shí)際使用時(shí)会通,發(fā)現(xiàn)setComponentEnabledSetting生效速度較慢口予,大概有3s左右。在3s內(nèi)啟動(dòng)應(yīng)用涕侈,會(huì)仍然調(diào)用原來(lái)的啟動(dòng)頁(yè)面沪停,導(dǎo)致3s退出應(yīng)用時(shí),將新建的任務(wù)棧清空裳涛,應(yīng)用退出木张。
原因了解了,通過(guò)代碼驗(yàn)證端三,確實(shí)可以借助上面的方式實(shí)現(xiàn)圖標(biāo)變更舷礼。
但是該方案還存在一個(gè)弊端:當(dāng)GuideActivity設(shè)置不可用時(shí),應(yīng)用內(nèi)其他頁(yè)面需要跳轉(zhuǎn)到GuideActivity時(shí)是不能實(shí)現(xiàn)的郊闯,同時(shí)也無(wú)法跳轉(zhuǎn)到activity-alias定義的NewGuideActivity中妻献,這個(gè)暫時(shí)沒(méi)有找到解決方案。
通過(guò)谷歌官方人員溝通团赁,了解到官方不建議通過(guò)使用activity-alias方式實(shí)現(xiàn)這種功能育拨,他們提供了一種新的方案。
最終方案
谷歌認(rèn)為欢摄,圖標(biāo)變更功能應(yīng)該使用獨(dú)立的LAUNCHER Activity實(shí)現(xiàn)熬丧,而不應(yīng)借助activity-alias。
最建議方案如下:
首先創(chuàng)建類(lèi)文件NewGuideActivity怀挠,實(shí)現(xiàn)如下代碼:
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>
這時(shí)候析蝴,NewGuideActivity就是一個(gè)真實(shí)的LAUNCHER了矗钟。由于NewGuideActivity直接繼承GuideActivity,本身沒(méi)有任何實(shí)質(zhì)代碼嫌变,所以功能也是完全一致的吨艇。對(duì)于NewGuideActivity、GuideActivity的設(shè)置和activity-alias方式類(lèi)似腾啥。這個(gè)能夠滿(mǎn)足變更的需求东涡。
以上相關(guān)代碼請(qǐng)參考開(kāi)源項(xiàng)目:睡眠助手