在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;
}