在Q以前,如果我們的應用是一個短信或撥號之類的特殊應用,想要通知用戶將我們的應用替換掉手機自帶的預搭載應用,TelecomManager.ACTION_CHANGE_DEFAULT_DIALER和Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT就可以幫我們實現。但在Q上,google改變這種處理方案,並推出了一個新特性——Role/RoleManager。從文檔介紹來看,是提供管理應用服務的,最常見的就是默認應用切換功能。
有了新的替代API,那麼原先的TelecomManager.ACTION_CHANGE_DEFAULT_DIALER和Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT還能使用嗎,很遺憾,雖然你可以寫出來,也可以發出去,但是當你運行這兩個action時,除了動作無響應外,你還會額外收到一條log:
Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT is removed for the calling package ,use RoleManager.createRequestRoleIntent() instead
即Q主動把你調用的action刪除掉,再暗示你該用Role了。這個log是由於PermissionPolicyService#isActionRemovedForCallingPackage()主動規避TelecomManager.ACTION_CHANGE_DEFAULT_DIALER和Telephony.Sms.Intents.ACTION_CHANGE_DEFAULT導致的,並且只在Q上生效。邏輯處理如下:
可以看出,這兩個action已經被內部無效化了,Role是你唯一可選的路。Role將可設置的默認應用細分成8類,每一類對應一個特定的Role類型。Role的展示頁面ReuqestRoleActivity針對調用發起者做了限制,只有同類型或者處於系統進程的應用才能調用Role服務。你用一個普通應用創建RoleManager,會發現它的isRoleAvailable()始終返回false的。
其實Role可以用一個通俗的例子來解釋,假設我們的應用是生旦淨末醜行當中的某個小角,戲臺上要從所有的旦角中選一個當默認主角,戲臺規定只有旦角行當的纔可以參與選擇。這個行當就是Role類型,戲臺就是androidQ系統,參與的方式就是調用RoleManager請求。
通過上述的比喻,就可以理解google這麼做的原因——減少服務的濫用,以往可以隨意調用的action都會被淘汰掉。爲此,針對Q我們的應用要通過配置權限或策略先成爲一個特定的短信、瀏覽器、或者桌面應用後,才能通過RoleManager切換系統同類型的默認應用。截止到Q Beta4版後,Role相關角色和權限對應表如下:
可以看出,作爲搭載的第一版,Role的類型並不是很成熟,還有類型存在未設置的狀態,後續肯定會有調整。我目前用到的只有ROLE_SMS和ROLE_CALL_SCREENING,下面就以這兩個角色爲例,跟大家分享下Role的使用和注意事項。
使用方法:
- ROLE_SMS
Manifest.xml中必須在啓動類中添加CATEGORY_DEFAULT和對應的角色API。
使用RoleManager獲得Intent對象,繼而請求Role處理,此時就可以彈出切換默認應用的彈窗了。具體代碼如下:
- ROLE_CALL_SCREENING
Manifest.xml中需要在CallScreeningService的實現類中聲明android.telecom.CallScreeningService。
<service android:name="你的應用中CallScreeningService的實現類" |
android:permission="android.permission.BIND_SCREENING_SERVICE"> |
<intent-filter> |
<action android:name="android.telecom.CallScreeningService"/> |
</intent-filter> |
</service>
|
CallScreeningService的實現類主要重寫onScreenCall(),這個方法是在受到來電時就會被觸發的,你可以在這裏對來電信息進行處理,將它封裝到CallIdentification中,CallIdentification特意增加了對來電騷擾程度的判斷,CallIdentification#setNuisanceConfidence()對應的級別共有5個,級別如下:
CONFIDENCE_NUISANCE 詐騙/騷擾電話
CONFIDENCE_LIKELY_NUISANCE 可能是詐騙/騷擾電話
CONFIDENCE_LIKELY_NOT_NUISANCE 可能不是詐騙/騷擾電話
CONFIDENCE_NOT_NUISANCE 非詐騙/騷擾電話
CONFIDENCE_UNKNOWN 未知來電
完整的一次來電信息處理代碼如下:
public class CallScreeningServiceImplementation extends CallScreeningService {
@Override
public void onScreenCall(@NonNull Call.Details details) {
//來電時該方法會被觸發,對來電信息進行設置
CallIdentification.Builder callIdentification = new CallIdentification.Builder();
//來電人名稱
callIdentification.setName("Name");
//來電其他信息
callIdentification.setDescription("Description");
callIdentification.setDetails("Details");
//來電類型
callIdentification.setNuisanceConfidence(CONFIDENCE_LIKELY_NOT_NUISANCE);
// 將修改好的來電信息向默認的電話應用發送過去
provideCallIdentification(details,callIdentification.build());
}
}
對應的電話應用一側,可以通過Call.Details.getCallIdentification()接受我們傳過去的數據。
注意事項:
- 使用Role的前提是,你的應用必須是Role指定的角色應用,比如android.intent.category.HOME將你的應用指定爲桌面應用,設置短信相關權限將你的應用指定爲SMS應用。
- 對於createRequestRoleIntent()返回的Intent,不能給它添加諸如FLAG_ACTIVITY_NEW_TASK之類的flag,並且必須使用startActivityForResult(Intent, int)進行調用,其他諸如startActivity()是無效的。否則你會收到如下所示的警告日誌:
package name cannot be null or empty: null
這個日誌產生的原因就在Role的響應類RequestRoleActivity中。我們發起的Intent,最 終會交給PermissionController來處理。這是一個APK,它和Settings存在,Role處理頁面是RequestRoleActivity。RequestRoleActivity#onCreate()中會獲取兩個參數,一個是mRoleName角色類型,就是我們在createRequestRoleIntent()中傳入的類型。一個是mPackageName呼叫的應用包名。它的獲取方式是getCallingPackage(),相關源碼如下:
getCallingPackage()依賴於是否調用了startActivityForResult()、是否有明確的返回值這兩個條件,只有這兩個條件都滿足的情況下,getCallingPackage()才能獲取到當前發起請求的包名。另外如果你使用了FLAG_ACTIVITY_NEW_TASK,RequestRoleActivity會在一個新的任務棧中加載導致它沒有返回值,同樣會導致getCallingPackage()返回null。因此調用Role的方式只能是在不添加任何flag的情況下使用startActivityForResult來調用,即使你不需要後續onActivityForResult中的邏輯。
- 一旦你的應用通過Role成功設置爲默認應用後,就無法再調用RoleManager進行切換了,這是因爲在RequestRoleActivity#onCreate()中會判斷調起的mPackageName是否已經是當前角色的持有者了。代碼如下:
RoleManager roleManager = getSystemService(RoleManager.class);
List<String> currentPackageNames = roleManager.getRoleHolders(mRoleName);
if (currentPackageNames.contains(mPackageName)) {
Log.i(LOG_TAG, "Application is already a role holder, role: " + mRoleName
+ ", package: " + mPackageName);
reportRequestResult(PermissionControllerStatsLog
.ROLE_REQUEST_RESULT_REPORTED__RESULT__IGNORED_ALREADY_GRANTED);
setResult(RESULT_OK);
finish();
return;
}