toast 在应用关闭通知显示时,某些手机上会不在显示toast;
原因简单的说就是toast使用了通知管理器INotificationManager类,而此类因为禁止了通知栏权限而不显示toast; 有兴趣的可以追下源码;
重点说下3种解决方法把:
提示用户打开通知栏权限 var CHECK_OP_NO_THROW: String = "checkOpNoThrow" var OP_POST_NOTIFICATION: String = "OP_POST_NOTIFICATION" fun isNotificationEnabled(): Boolean { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return (Utils.getContext().getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).areNotificationsEnabled() } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { var mAppOps = Utils.getContext().getSystemService(Context.APP_OPS_SERVICE) var appInfo: ApplicationInfo = Utils.getContext().getApplicationInfo() var pkg: String = Utils.getContext().getApplicationContext().getPackageName() var uid: Int = appInfo.uid var appOpsClass: Class<*>? = null /* Context.APP_OPS_MANAGER */ try { appOpsClass = Class.forName(AppOpsManager::class.java.name) var checkOpNoThrowMethod: Method = appOpsClass.getMethod(CHECK_OP_NO_THROW, Integer.TYPE, Integer.TYPE, String::class.java) var opPostNotificationValue: Field = appOpsClass.getDeclaredField(OP_POST_NOTIFICATION) var value: Int = opPostNotificationValue.get(Integer::class.java) as Int return (checkOpNoThrowMethod.invoke(mAppOps, value, uid, pkg) as Int == AppOpsManager.MODE_ALLOWED) } catch (e: Exception) { e.printStackTrace() } } return true } 完全自定义toast或者弹框,规避通知管理器限制如 自定义toast解决通知栏关闭
INotificationManager 根据包名判断提示权限,使用动态代理使用android替换包名达到允许弹框的目的;见 showSystemToast
/** * 显示系统Toast */ private static void showSystemToast(Toast toast){ try{ Method getServiceMethod = Toast.class.getDeclaredMethod("getService"); getServiceMethod.setAccessible(true); final Object iNotificationManager = getServiceMethod.invoke(null); Class iNotificationManagerCls = Class.forName("android.app.INotificationManager"); Object iNotificationManagerProxy = Proxy.newProxyInstance(toast.getClass().getClassLoader(), new Class[]{iNotificationManagerCls}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 强制使用系统Toast // 华为p20 pro上为enqueueToastEx if("enqueueToast".equals(method.getName()) || "enqueueToastEx".equals(method.getName())){ args[0] = "android"; } return method.invoke(iNotificationManager, args); } }); Field sServiceFiled = Toast.class.getDeclaredField("sService"); sServiceFiled.setAccessible(true); sServiceFiled.set(null, iNotificationManagerProxy); toast.show(); }catch (Exception e){ e.printStackTrace(); } }个人偏向第三种动态代理的方案,具体使用判断是否开启通知栏权限使用第一种或者第三种方案;
记录下toast为什么打不开的原因吧, read the fuck source code ~
--------------------------------toast 的 show方法---------------------------- 最终获取 `NotificationManagerService` service; /** * Show the view for the specified duration. */ public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } } static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; } ---------------------NotificationManagerService 的 enqueueToast 方法---------------------- public void enqueueToast(String pkg, ITransientNotification callback, int duration) { if (DBG) { Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback + " duration=" + duration); } if (pkg == null || callback == null) { Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback); return ; } //通过动态代理更改了包名为android后,isSystemToasta为true,可规避下面的reture; final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); final boolean isPackageSuspended = isPackageSuspendedForUser(pkg, Binder.getCallingUid()); //此行会根据包名判断通知栏的权限是否禁用; if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid()) || isPackageSuspended)) { if (!isSystemToast) { Slog.e(TAG, "Suppressing toast from package " + pkg + (isPackageSuspended? " due to package suspended by administrator." : " by user request.")); return; } } synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); // If it's already in the queue, we update it in place, we don't // move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { // Limit the number of toasts that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } } record = new ToastRecord(callingPid, pkg, callback, duration); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveLocked(callingPid); } // If it's at index 0, it's the current toast. It doesn't matter if it's // new or just been updated. Call back and tell it to show itself. // If the callback fails, this will remove it from the list, so don't // assume that it's valid after this. if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } } }解决方法很巧妙,实测可以用,学习了;