ActivityManagerService对于app缺少运行时权限而crash的一种友好处理方法
我们都知道runtime权限是google在android上权限管理的又一大重要改变,在应用程序安装过程中,只会grant install部分的基本权限,而对于dangerous的权限,应用程序需要在运行时主动申请,并动态的由用户来确认是否需要给予对应的权限。
当然,google在开发者文档中也详细的介绍了关于新的权限申请机制,也给app开发人员带来了新的机遇与挑战,但是对于很多旧版本的app或者说一些初出茅庐的开发者开发的应用,并没有按照google的要求去设计兼容性很强的app时,这些应用一旦跑起来,去访问需要运行时给予权限的操作而导致应用的crash,这些用户就会看到一个很不想看到的结果,应用直接crash了,如果不懂开发的用户用到,他根本不知道发生了什么,只见眼前一黑,应用闪退了。。。。Unfortunately, Your application has stopped!!
这真让人绝望啊。
既然app开发者没有做好这件事情,我作为一个系统开发工程师(虽然对于给应用擦屁股这种事我是极度拒绝的),但是,对于系统机制的探索使我对这一问题产生了浓厚的兴趣,以便可以为该问题提供一个系统级的友好解决方案。于是,顺着AMS的源码,我们今天就为那些不友好的app开发者捏着鼻子擦一次屁股吧。
我们知道,zygote启动时,会为应用程序主线程注册一个UncaughtHandler,当应用程序发生异常时,会触发RuntimeInit的UncaughtHandler的uncaughtException方法:
01-02 13:06:40.072 2688-2688/linhui.skysoft.com.permissiontest E/AndroidRuntime: FATAL EXCEPTION: main
Process: linhui.skysoft.com.permissiontest, PID: 2688
java.lang.RuntimeException: Unable to resume activity {linhui.skysoft.com.permissiontest/linhui.skysoft.com.permissiontest.MainActivity}: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.ContactsProvider2 from ProcessRecord{b19ead7 2688:linhui.skysoft.com.permissiontest/u0a79} (pid=2688, uid=10079) requires android.permission.READ_CONTACTS or android.permission.WRITE_CONTACTS
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3103)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3134)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2481)
at android.app.ActivityThread.access$900(ActivityThread.java:150)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.ContactsProvider2 from ProcessRecord{b19ead7 2688:linhui.skysoft.com.permissiontest/u0a79} (pid=2688, uid=10079) requires android.permission.READ_CONTACTS or android.permission.WRITE_CONTACTS
at android.os.Parcel.readException(Parcel.java:1620)
at android.os.Parcel.readException(Parcel.java:1573)
at android.app.ActivityManagerProxy.getContentProvider(ActivityManagerNative.java:3550)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:4778)
at android.app.ContextImpl$ApplicationContentResolver.acquireProvider(ContextImpl.java:1999)
at android.content.ContentResolver.acquireProvider(ContentResolver.java:1455)
at android.content.ContentResolver.acquireContentProviderClient(ContentResolver.java:1520)
at android.content.ContentResolver.applyBatch(ContentResolver.java:1268)
at linhui.skysoft.com.permissiontest.MainActivity.insertDummyContact(MainActivity.java:157)
at linhui.skysoft.com.permissiontest.MainActivity.onResume(MainActivity.java:46)
at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1259)
at android.app.Activity.performResume(Activity.java:6361)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3092)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3134)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2481)
at android.app.ActivityThread.access$900(ActivityThread.java:150)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
这个方法会调用ActivityManagerNative的如下方法继而由ActivityManagerService来对其进行处理:
ActivityManagerNative.getDefault().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.CrashInfo(e));
ActivityManagerNative.getDefault()获取到的是IActivityManagerProxy的binder代理,
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
因此,接下来通过binder驱动,将会唤醒AMS的handleApplicationCrash来处理此次crash。
public void handleApplicationCrash(IBinder app, ApplicationErrorReport.CrashInfo crashInfo) {
ProcessRecord r = findAppProcess(app, "Crash");
final String processName = app == null ? "system_server"
: (r == null ? "unknown" : r.processName);
handleApplicationCrashInner("crash", r, processName, crashInfo);
}
/* Native crash reporting uses this inner version because it needs to be somewhat
* decoupled from the AM-managed cleanup lifecycle
*/
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
ApplicationErrorReport.CrashInfo crashInfo) {
EventLog.writeEvent(EventLogTags.AM_CRASH, Binder.getCallingPid(),
UserHandle.getUserId(Binder.getCallingUid()), processName,
r == null ? -1 : r.info.flags,
crashInfo.exceptionClassName,
crashInfo.exceptionMessage,
crashInfo.throwFileName,
crashInfo.throwLineNumber);
addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo);
crashApplication(r, crashInfo);
}
这里通过层层封装调用,最终会调用到crashApplication方法中。这里的r为进程的信息,类型为ProcessRecord,而crashinfo中则包含了crash原因以及调用栈。
我们既然拿到了crash信息以及crash调用栈, 就可以根据crash信息处理特殊的因为权限而导致的异常了。
crashApplication方法主要是构造一个AppErrorResult信息,并封装一个SHOW_ERR_MSG的消息到UIHandler中去处理。
private void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
long timeMillis = System.currentTimeMillis();
String shortMsg = crashInfo.exceptionClassName;
String longMsg = crashInfo.exceptionMessage;
String stackTrace = crashInfo.stackTrace;
if (shortMsg != null && longMsg != null) {
longMsg = shortMsg + ": " + longMsg;
} else if (shortMsg != null) {
longMsg = shortMsg;
}
Slog.i(TAG,"crashApplication ==>",new Throwable());
Slog.i(TAG,"crashApplication, shortMsg:"+shortMsg+" longMsg:"+longMsg);
AppErrorResult result = new AppErrorResult();
synchronized (this) {
if (mController != null) {
try {
String name = r != null ? r.processName : null;
int pid = r != null ? r.pid : Binder.getCallingPid();
int uid = r != null ? r.info.uid : Binder.getCallingUid();
if (!mController.appCrashed(name, pid,
shortMsg, longMsg, timeMillis, crashInfo.stackTrace)) {
if ("1".equals(SystemProperties.get(SYSTEM_DEBUGGABLE, "0"))
&& "Native crash".equals(crashInfo.exceptionClassName)) {
Slog.w(TAG, "Skip killing native crashed app " + name
+ "(" + pid + ") during testing");
} else {
Slog.w(TAG, "Force-killing crashed app " + name
+ " at watcher's request");
if (r != null) {
r.kill("crash", true);
} else {
// Huh.
Process.killProcess(pid);
killProcessGroup(uid, pid);
}
}
return;
}
} catch (RemoteException e) {
mController = null;
Watchdog.getInstance().setActivityController(null);
}
}
final long origId = Binder.clearCallingIdentity();
// If this process is running instrumentation, finish it.
if (r != null && r.instrumentationClass != null) {
Slog.w(TAG, "Error in app " + r.processName
+ " running instrumentation " + r.instrumentationClass + ":");
if (shortMsg != null) Slog.w(TAG, " " + shortMsg);
if (longMsg != null) Slog.w(TAG, " " + longMsg);
Bundle info = new Bundle();
info.putString("shortMsg", shortMsg);
info.putString("longMsg", longMsg);
finishInstrumentationLocked(r, Activity.RESULT_CANCELED, info);
Binder.restoreCallingIdentity(origId);
return;
}
// Log crash in battery stats.
if (r != null) {
mBatteryStatsService.noteProcessCrash(r.processName, r.uid);
}
// If we can't identify the process or it's already exceeded its crash quota,
// quit right away without showing a crash dialog.
if (r == null || !makeAppCrashingLocked(r, shortMsg, longMsg, stackTrace)) {
Binder.restoreCallingIdentity(origId);
return;
}
/// linhui:Permission exception dialog, @{
result.setExceptionMsg(longMsg);
/// @}
Message msg = Message.obtain();
msg.what = SHOW_ERROR_MSG;
HashMap data = new HashMap();
data.put("result", result);
data.put("app", r);
msg.obj = data;
mUiHandler.sendMessage(msg);
Binder.restoreCallingIdentity(origId);
}
在UIHandler的handlemessage中,则会显示我们app crash的dialog。
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_ERROR_MSG: {
.......
if (mShowDialogs && !mSleeping && !mShuttingDown) {
Dialog d = new AppErrorDialog(mContext,
ActivityManagerService.this, res, proc);
d.show();
proc.crashDialog = d;
} else {
.......
}
}
}
} break;
所以到达这里后,我们可以通过定制AppErrorDialog的消息框,来达到友好的目的。这里是通过获取到调用栈,解析SecurityException的信息,解析出权限所属组,对于 dialog的选项框重定向到应用的权限管理设置中,提醒用户打开对应的权限再启动该app,以实现较好的用户体验。
我们在crashApplication中,构造AppErrorResult的时候设置其crash原因(即解析的crash字串),再由AppErrorDialog去解析,将解析的结果在dialog中友好的显示出来,最后,如果解析发现是由于Permission Denial导致的,则启动权限管理的intent到达app权限管理设置中去,以便提醒用户手动开启应用需要的权限。
具体的实践过程如下:
1. 在AppErrorResult中新增字符串mExceptionMsg,该字符串中保存的是crashinfo的longMsg,通过这个消息,我们基本上可以确定crash的app,以及具体的crash原因,如下所示:
longMsg:java.lang.SecurityException: Permission Denial: opening provider com.android.providers.contacts.ContactsProvider2 from ProcessRecord{6e38cac 2733:linhui.skysoft.com.permissiontest/u0a79} (pid=2733, uid=10079) requires android.permission.READ_CONTACTS or android.permission.WRITE_CONTACTS
--- a/services/core/java/com/android/server/am/AppErrorResult.java
+++ b/services/core/java/com/android/server/am/AppErrorResult.java
@@ -37,6 +37,20 @@ final class AppErrorResult {
return mResult;
}
+ /// linhui:Permission exception dialog, @{
+ public synchronized void setExceptionMsg(String msg) {
+ mExceptionMsg = msg;
+ }
+
+ public synchronized String getExceptionMsg() {
+ return mExceptionMsg;
+ }
+
+ String mExceptionMsg = null;
+ /// @}
+
2. 然后再构造AppErrorDialog中去解析上面的crash信息,得到具体的appname,permission等。
下面这段代码主要是解析mExceptionMsg各个字段。
int crashByMustHavePermission = CRASH_BY_PERMISSION_NONE;
+ String permissionTitled = null;
+ if (mExceptionMsg != null
+ && mExceptionMsg.contains(SECURITY_EXCEPTION)) {
+ if (DEBUG_PERMISSION) {
+ Slog.v(TAG, "AppErrorDialog mExceptionMsg = " + mExceptionMsg);
+ }
+ if (mExceptionMsg.contains(SECURITY_SUB_PERMISSION_DENIAL)) {
+ int startIndex = mExceptionMsg.indexOf(SECURITY_SUB_REQUIRES)
+ + SECURITY_SUB_REQUIRES.length();
+ String parseResult = mExceptionMsg.substring(startIndex, mExceptionMsg.length());
+ if (parseResult.contains(SECURITY_SUB_OR)) {
+ startIndex = parseResult.indexOf(SECURITY_SUB_OR) +
+ SECURITY_SUB_OR.length();
+ parseResult = parseResult.substring(startIndex, parseResult.length());
+ }
+
+ permissionTitled = getPermissionTitle(context.getPackageManager(),
+ parseResult);
+
+ if (DEBUG_PERMISSION) {
+ Slog.v(TAG, "AppErrorDialog parseResult = " + parseResult +
+ " and permissionTitled = " + permissionTitled);
+ }
+ if (permissionTitled != null) {
+ crashByMustHavePermission = CRASH_BY_PERMISSION_DETAIL;
+ }
+ }
+ if (crashByMustHavePermission == CRASH_BY_PERMISSION_NONE) {
+ crashByMustHavePermission = CRASH_BY_PERMISSION_TRY;
+ }
+ }
3. 通过封装对应的message将消息发给handler去处理,这里新建了一个专门处理PERMISSION_SETTINGS的消息:
if (crashByMustHavePermission != CRASH_BY_PERMISSION_NONE) {
setButton(DialogInterface.BUTTON_POSITIVE,
res.getText(com.android.internal.R.string.force_close),
mHandler.obtainMessage(PERMISSION_SETTINGS, app.info.packageName));
} else {
setButton(DialogInterface.BUTTON_POSITIVE,
res.getText(com.android.internal.R.string.force_close),
mHandler.obtainMessage(FORCE_QUIT));
}
}
.........
public void handleMessage(Message msg) {
/// linhui:Permission exception dialog, @{
if (msg.what == PERMISSION_SETTINGS) {
Intent mIntent = new Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS);
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, (String) msg.obj);
mContext.startActivity(mIntent);
}
最后,在handleMessage中对PERMISSION_SETTINGS的消息定向到app的权限管理界面中,提醒用户打开具体所缺的权限。
这样app再因为缺少对应的权限而crash的情况得到了友好的解决。
具体效果如下:
可以看到,我们定制后的dialog中会友好的提示,PermissionTest应用由于缺少访问Contacts的权限而死掉,现在通过OK按钮,我们便可直接达到应用权限管理界面打开对应的权限,保证app的正常运行。
怎么样,是不是很简单?虽然如此,我们还是强烈建议app开发者们遵循google的设计原则主动申请应用的运行时权限,以便开发出兼容性超强的app,做一个合格的Developer!
关于应用权限申请的部分,可以参考我的另一篇博文: