java.lang.SecurityException:Perm
记录一个深坑
最近发的版本线上突然多了好多支付报错的异常。
其中一部分
这只是其中一部分,2天时间影响大概30-40个用户。
但是看错误的回调栈很奇怪。
image.png
光看回调栈,基本就知道了被start的Activity的清单文件Manifest的exported属性肯定设置了false。
为了验证,我特意下载了DANA的app,打开了他们的Manifest文件。
kXMbptKVGi.jpg
结果意料之中,又有一些意料之外,然后看这个app的发版时间
![](https://img.haomeiwen.com/i4487915/c88218cc2b61fef7.png)
确实是最近才发布的新版本。但是看错误日志Intent的data里的uri是https://wsa.wallet.airpay.co.id,打开这个链接你会发现这个是shopee的网站。也就是说这个支付行为不是DANA渠道的。
我们知道https的跳转默认属于浏览器行为,这个属于系统默认行为,除非用户主动指定默认浏览器行为。
- 指定默认浏览器行为在某些系统上可以通过app的意图选择器。
- 主要还是可以通过系统设置->apps->选择默认app->选择默认浏览器,指定浏览器执行app。
- 当然还有第三种情况,禁用系统浏览器,导致无可默认可执行意图,那么系统选择意图会去找可以执行该意图的app。
- 当然还可以在app内部通过指定ComponentName指定可以执行的app,这种方式需要再清单文件里声明queries标签增加指定app包名。
很明显,第一种和第二种不太可能,因为DANA没有声明Browser行为,第四种也不可能,因为发起app是我自己写的。
所以只有第三种可能,系统浏览器被禁用了,模拟一下尝试还原崩溃场景。
禁用默认浏览器,添加启动Shopee代码。
startActivity(Intent.parseUri("https://wsa.wallet.airpay.co.id",Intent.FLAG_ACTIVITY_NEW_TASK))
img_v2_63a9d6e9-5c94-4e7d-985d-8005f96ff57g.jpg
结果意料之中,确实还原触发了线上崩溃。对于这种外部影响导致的崩溃确实有些坑爹,本质上是因为用户的不规范行为禁用系统浏览器(但是这种用户不在少数,看线上影响用户达到40+就能看出来),以及DANA app的不规范声明导致的(但DANA的清单文件声明其实是合规的,所以本质上我觉得还是android系统Bug)。
既然找到了问题,解决方案也就好说。
- 自己在清单文件声明一个https处理activity,exported声明为true,好处是可以引流,坏处是也可能会被用户认为是流氓软件。
- 在启动deeplink之前,判断shema是否为https,如果是则打开app内置浏览器。
第二种没什么好说的代码太简单。
第一种方式,我们通过查询
<activity
android:name=".activity.WebViewActivity"
android:theme="@style/Theme.BaseProject"
android:screenOrientation="portrait"
android:exported="true"
android:configChanges="orientation|keyboardHidden|fontScale|navigation|layoutDirection|locale"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" >
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent-filter>
</activity>
image.png
通过查看出错源码我们知道最终会调用Android11以下通过ActivityStackSupervisor,Android11以上通过ActivityTaskSupervisor
的checkStartAnyActivityPermission去检查权限,代码差不多,可以从代码中看到我们崩溃的代码是因为a info.exprted=false导致的。
boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo, String resultWho,
int requestCode, int callingPid, int callingUid, String callingPackage,
@Nullable String callingFeatureId, boolean ignoreTargetSecurity,
boolean launchingInTask, WindowProcessController callerApp, ActivityRecord resultRecord,
Task resultRootTask) {
final boolean isCallerRecents = mService.getRecentTasks() != null
&& mService.getRecentTasks().isCallerRecents(callingUid);
final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid,
callingUid);
if (startAnyPerm == PERMISSION_GRANTED || (isCallerRecents && launchingInTask)) {
// If the caller has START_ANY_ACTIVITY, ignore all checks below. In addition, if the
// caller is the recents component and we are specifically starting an activity in an
// existing task, then also allow the activity to be fully relaunched.
return true;
}
final int componentRestriction = getComponentRestrictionForCallingPackage(aInfo,
callingPackage, callingFeatureId, callingPid, callingUid, ignoreTargetSecurity);
final int actionRestriction = getActionRestrictionForCallingPackage(
intent.getAction(), callingPackage, callingFeatureId, callingPid, callingUid);
if (componentRestriction == ACTIVITY_RESTRICTION_PERMISSION
|| actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
if (resultRecord != null) {
resultRecord.sendResult(INVALID_UID, resultWho, requestCode,
Activity.RESULT_CANCELED, null /* data */, null /* dataGrants */);
}
final String msg;
if (actionRestriction == ACTIVITY_RESTRICTION_PERMISSION) {
msg = "Permission Denial: starting " + intent.toString()
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ")" + " with revoked permission "
+ ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction());
}
//
//看的出来我们的出错源码msg来自于这里
//aInfo就是ActivityInfo,ActivityInfo中含有exported标志
else if (!aInfo.exported) {
msg = "Permission Denial: starting " + intent.toString()
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ")"
+ " not exported from uid " + aInfo.applicationInfo.uid;
} else {
msg = "Permission Denial: starting " + intent.toString()
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ")"
+ " requires " + aInfo.permission;
}
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
if (actionRestriction == ACTIVITY_RESTRICTION_APPOP) {
final String message = "Appop Denial: starting " + intent.toString()
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ")"
+ " requires " + AppOpsManager.permissionToOp(
ACTION_TO_RUNTIME_PERMISSION.get(intent.getAction()));
Slog.w(TAG, message);
return false;
} else if (componentRestriction == ACTIVITY_RESTRICTION_APPOP) {
final String message = "Appop Denial: starting " + intent.toString()
+ " from " + callerApp + " (pid=" + callingPid
+ ", uid=" + callingUid + ")"
+ " requires appop " + AppOpsManager.permissionToOp(aInfo.permission);
Slog.w(TAG, message);
return false;
}
return true;
}
所以我们的最终的启动deeplink的逻辑就可以通过查询过滤进行。
//不过首先需要判断是否有默认启动app
var intent = Intent.parseUri("https://wsa.wallet.airpay.co.id",Intent.FLAG_ACTIVITY_NEW_TASK)
var defaultStartApp = packageManager.resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY)
if(defaultStartApp == null){
val enableStartList = packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
if(enableStartList != null && enableStartList.isEmpty().not()){
defaultStartApp = enableStartList[0]
}
}
defaultStartApp?.let {
startActivity(intent.apply {
setClassName(it.activityInfo.packageName,it.activityInfo.name)
})
}
queryIntentActivities的第二个参数表示过滤标志位,传0则默认返回所有,也可以通过MATCH_DIRECT_BOOT_AUTO查询可自动启动的app行为,这里返回的值主要是在清单文件里加了这个属性的,以及系统默认可执行schema行为app包信息。
![](https://img.haomeiwen.com/i4487915/9ad9a149767780a0.png)
我们可以通过MATCH_DEFAULT_ONLY标志位过滤掉exported为空的情况,保险起见,在返回结果集合中对非exported进行过滤。
packageManager.queryIntentActivities(
Intent.parseUri("https://wsa.wallet.airpay.co.id",Intent.FLAG_ACTIVITY_NEW_TASK),
PackageManager.MATCH_DIRECT_BOOT_AUTO)
.filter { it.activityInfo.exported }
好了,以上就是解决android系统坑问题的过程了。