美文网首页反编译 hook 破解
Hook Toast解决9.0以下机型通知关闭无法显示Toast

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

作者: Endless_123 | 来源:发表于2019-03-13 13:51 被阅读0次

    本文对照的源码是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

        本文链接:https://www.haomeiwen.com/subject/aevnpqtx.html