Hook Toast解决9.0以下机型通知关闭无法显示Toast

本文对照的源码是8.0.0_r1
通常我们会这样使用Toast

Toast.makeText(context, message, duration).show()

而当我们在手机设置中取消你的应用通知管理时,就会发现Toast消失了。下面我们从源码的角度分析Toast的调用与被系统屏蔽。

首先会调用Toast中的show()方法

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }
        //1
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            //2
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

在注释1处我们会通过getService()获得INotificationManager,在getService()中通过AIDL跨进程获取通知服务管理。并在注释2中调用enqueueToast方法。由系统启动源码我们知道NotificationManagerService是在SystemServer进程中启动,下面看一下NotificationManagerService。在源码的frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService

private final IBinder mService = new INotificationManager.Stub() {
        // Toasts
         @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
           
            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }
            //1是否是系统弹窗
            final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
            //2 弹窗依据
            final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());
            //3在这里判断是否允许你的应用弹窗
            if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                    (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                            || isPackageSuspended)) {
                Slog.e(TAG, "Suppressing toast from package " + pkg
                        + (isPackageSuspended
                                ? " due to package suspended by administrator."
                                : " by user request."));
                return;
            }

            //省略不相关代码...
        }
}

在NotificationManagerService源码中我们可以看到在注释1处判断了是否为系统级弹窗,判断进程id是否是系统进程,或者包名是否是'android',这里为我们Toast弹窗伪装成系统弹窗提供了方法,在后面我们会说到。在注释2处调用了isPackageSuspendedForUser(pkg, Binder.getCallingUid()),在注释3处判断是否可以弹窗。先看一下注释2处的方法最终会调用frameworks/base/services/core/java/com/android/server/pm/PackageManagerService

    @Override
    public boolean isPackageSuspendedForUser(String packageName, int userId) {
        final int callingUid = Binder.getCallingUid();
        //1 检查用户权限
        enforceCrossUserPermission(callingUid, userId,
                true /* requireFullPermission */, false /* checkShell */,
                "isPackageSuspendedForUser for user " + userId);
        synchronized (mPackages) {
            final PackageSetting ps = mSettings.mPackages.get(packageName);
            if (ps == null || filterAppAccessLPr(ps, callingUid, userId)) {
                throw new IllegalArgumentException("Unknown target package: " + packageName);
            }
            //2根据线程id返回是否暂停
            return ps.getSuspended(userId);
        }
    }

在注释1处isPackageSuspendedForUser方法会调用PackageManager.isPackageSuspendedForUser检查用户交互权限“android.Manifest.permission.INTERACT_ACROSS_USERS_FULL” 。

   private boolean isPackageSuspendedForUser(String pkg, int uid) {
        int userId = UserHandle.getUserId(uid);
        try {
            return mPackageManager.isPackageSuspendedForUser(pkg, userId);
        } catch (RemoteException re) {
            throw new SecurityException("Could not talk to package manager service");
        } catch (IllegalArgumentException ex) {
            // Package not found.
            return false;
        }
    }

在注释2处在最后会调用PackageSetting的getSuspended获取当前用户进程状态是否是暂停。暂停时也是不能发送Toast的。

最后分析不能发送Toast最后一个依据areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())这个方法会判断通知是否开启,未开启不能Toast。


上面介绍了Toast能否弹出的情况,通过源码分析在通知关闭时是无法Toast的,要想应用在关闭通知的情况下Toast我们需要伪装成系统应用。这里我们用hook进行替换Toast.enqueueToast方法的参数伪装成系统Toast。

  try {
            //1通过反射获取Toast的getService方法
            Method serviceMethod = Toast.class.getDeclaredMethod("getService");
            serviceMethod.setAccessible(true);
                //2调用 toast 中的getService() 方法 返回INotificationManager类型的Object
                Object iNotificationManagerObj = serviceMethod.invoke(toast);
                //3反射获取INotificationManager的Class
                Class iNotificationManagerCls = Class.forName("android.app.INotificationManager");
                //4创建 INotificationManager的代理对象 替换Toast中的 INotificationManager
                Object iNotificationManagerProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNotificationManagerCls},
                        (proxy, method, args) -> {
                            //强制使用系统Toast
                            if ("enqueueToast".equals(method.getName())
                                    || "enqueueToastEx".equals(method.getName())) {  //华为p20 pro上为enqueueToastEx
                                //5上文中pkg 为“android”时为系统弹窗
                                args[0] = "android";
                            }
                            Log.e("test", "强制使用系统Toast>>>>>>>>>");
                            return method.invoke(iNotificationManagerObj, args);
                        });
                //6进行替换
                Field sServiceFiled = Toast.class.getDeclaredField("sService");
                sServiceFiled.setAccessible(true);
                sServiceFiled.set(toast, iNotificationManagerProxy);
            }
            toast.show();
        } catch (Exception e) {
            e.printStackTrace();
        }

到此我们从源码分析了Toast失效到,用hook解决Toast的失效,源码会在封装后上传至github。感谢大佬 刘望舒的《Android进阶解密》在framework层阅读源码及了解整个Android系统的启发,不在惧怕底层源码,要做到知其然知其所以然。

你可能感兴趣的:(Hook Toast解决9.0以下机型通知关闭无法显示Toast)