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!

关于应用权限申请的部分,可以参考我的另一篇博文:

Android M PackageManager应用程序权限管理源码剖析及runtime permission实战

posted @ 2018-09-19 15:27  mail181  阅读(31)  评论(0编辑  收藏  举报