Tip: after the article is written, the directory can be generated automatically. Please refer to the help document on the right for how to generate it
preface
I really can't write a preface, so I go straight to the point
Tip: This article is based on Android API 25
1, WindowManager BadTokenException
Toast has a chance to throw this exception on Android API 25
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_toast_window_manager_bad_token_exception) Toast.makeText(this, "test Toast Exception", Toast.LENGTH_SHORT).apply { fixWindowManagerBadTokenExceptionIn(this) }.show() // Here sleep 2 seconds TimeUnit.SECONDS.sleep(2) LogUtils.i(TAG, "Print after hibernation") }
The above code must show this exception on the device on Android API 25
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@cdadaf1 is not valid; is your activity running? at android.view.ViewRootImpl.setView(ViewRootImpl.java:990) at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:533) at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:95) at android.widget.Toast$TN.handleShow(Toast.java:504) at android.widget.Toast$TN$2.handleMessage(Toast.java:387) at android.os.Handler.dispatchMessage(Handler.java:102) at com.study.ToastWindowManagerBadTokenExceptionActivity$SafelyHandlerWrapper.dispatchMessage(ToastWindowManagerBadTokenExceptionActivity.kt:64) at android.os.Looper.loop(Looper.java:159) at android.app.ActivityThread.main(ActivityThread.java:6385) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1096) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:883)
Exception description Android view. WindowManager$BadTokenException: Unable to add window – token android. os. BinderProxy@cdadaf1 is not valid; is your activity running? Let's analyze the reasons
public static Toast makeText(Context context, CharSequence text, @Duration int duration) { Toast result = new Toast(context); LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); tv.setText(text); result.mNextView = v; result.mDuration = duration; return result; }
makeText is to create a Toast object and inflate a View
public Toast(Context context) { mContext = context; mTN = new TN(); mTN.mY = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.toast_y_offset); mTN.mGravity = context.getResources().getInteger( com.android.internal.R.integer.config_toastDefaultGravity); }
Create TN object in Toast construction method
TN() { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. final WindowManager.LayoutParams params = mParams; params.height = WindowManager.LayoutParams.WRAP_CONTENT; params.width = WindowManager.LayoutParams.WRAP_CONTENT; params.format = PixelFormat.TRANSLUCENT; params.windowAnimations = com.android.internal.R.style.Animation_Toast; params.type = WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; }
In the construction method of TN, only a WindowManager is created The layoutparams object type is WindowManager LayoutParams. TYPE_ Toast, look back at the show method
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 } }
Continue to look at the getService method
private static INotificationManager sService; static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
Here is the implementation of AIDL. The specific implementation is com android. server. notification. Notificationmanagerservice#mservice calls its enqueueToast method
public void enqueueToast(String pkg, ITransientNotification callback, int duration) { // Omit part of the code and analyze the second problem in this part synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); if (index >= 0) { // to update record = mToastQueue.get(index); record.update(duration); } else { 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++; // The maximum number of Toast lists for non system applications is 50 if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } } Binder token = new Binder(); // Create a token and store it in a HashMap mWindowManagerInternal.addWindowToken(token, WindowManager.LayoutParams.TYPE_TOAST); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveIfNeededLocked(callingPid); } // Show next Toast if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } } }
The above mWindowManagerInternal implementation is com android. server. wm. WindowManagerService. LocalService
public void addWindowToken(IBinder token, int type) { WindowManagerService.this.addWindowToken(token, type); } public void addWindowToken(IBinder token, int type) { if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS, "addWindowToken()")) { throw new SecurityException("Requires MANAGE_APP_TOKENS permission"); } synchronized(mWindowMap) { WindowToken wtoken = mTokenMap.get(token); if (wtoken != null) { Slog.w(TAG_WM, "Attempted to add existing input method token: " + token); return; } // Create WindowToken object wtoken = new WindowToken(this, token, type, true); // Add to HashMap mTokenMap.put(token, wtoken); if (type == TYPE_WALLPAPER) { mWallpaperControllerLocked.addWallpaperToken(wtoken); } } }
Take another look at the method showNextToastLocked that displays the next Toast
void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { // The callback here is the TN object passed record.callback.show(record.token); // Send delay message to remove Toast scheduleTimeoutLocked(record); return; } catch (RemoteException e) { // Omit some codes } } }
Let's take a look at TN's show method first
public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(0, windowToken).sendToTarget(); } final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { IBinder token = (IBinder) msg.obj; handleShow(token); } }; public void handleShow(IBinder windowToken) { if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView=" + mNextView); if (mView != mNextView) { // remove the old view if necessary handleHide(); mView = mNextView; Context context = mView.getContext().getApplicationContext(); String packageName = mView.getContext().getOpPackageName(); if (context == null) { context = mView.getContext(); } mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); // We can resolve the Gravity here by using the Locale for getting // the layout direction final Configuration config = mView.getContext().getResources().getConfiguration(); final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection()); mParams.gravity = gravity; if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) { mParams.horizontalWeight = 1.0f; } if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) { mParams.verticalWeight = 1.0f; } mParams.x = mX; mParams.y = mY; mParams.verticalMargin = mVerticalMargin; mParams.horizontalMargin = mHorizontalMargin; mParams.packageName = packageName; mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; mParams.token = windowToken; if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); mWM.removeView(mView); } if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this); // Through WindowManager addView, the mWM here is Android view. WindowManagerImpl mWM.addView(mView, mParams); trySendAccessibilityEvent(); } }
Take a look at the addView method of WindowManagerImpl
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { // Omit some codes ViewRootImpl root; View panelParentView = null; synchronized (mLock) { // Omit some codes // Create a ViewRootImpl object. ViewRootImpl is not a View, but it is the parent of the top-level View // When all views are refreshed, they are refreshed layer by layer by finding ViewRootImpl through getParent root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); } // do this last because it fires off messages to start doing things try { // Call setView of ViewRootImpl root.setView(view, wparams, panelParentView); } catch (RuntimeException e) { // BadTokenException or InvalidDisplayException, clean up. synchronized (mLock) { final int index = findViewLocked(view, false); if (index >= 0) { removeViewLocked(index, true); } } throw e; } }
public ViewRootImpl(Context context, Display display) { mContext = context; // mWindowSession is created here. The specific implementation is com android. server. wm. Session mWindowSession = WindowManagerGlobal.getWindowSession(); // Omit some codes }
Next, look at the setView method of ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; // Omit some codes try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { mAdded = false; mView = null; mAttachInfo.mRootView = null; mInputChannel = null; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); throw new RuntimeException("Adding window failed", e); } finally { if (restore) { attrs.restore(); } } // Omit some codes if (res < WindowManagerGlobal.ADD_OKAY) { mAttachInfo.mRootView = null; mAdded = false; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); switch (res) { // According to the exception information thrown above, this case must have been triggered case WindowManagerGlobal.ADD_BAD_APP_TOKEN: case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN: throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token + " is not valid; is your activity running?"); case WindowManagerGlobal.ADD_NOT_APP_TOKEN: throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token + " is not for an application"); case WindowManagerGlobal.ADD_APP_EXITING: throw new WindowManager.BadTokenException( "Unable to add window -- app for token " + attrs.token + " is exiting"); case WindowManagerGlobal.ADD_DUPLICATE_ADD: throw new WindowManager.BadTokenException( "Unable to add window -- window " + mWindow + " has already been added"); case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED: // Silently ignore -- we would have just removed it // right away, anyway. return; case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON: throw new WindowManager.BadTokenException("Unable to add window " + mWindow + " -- another window of type " + mWindowAttributes.type + " already exists"); case WindowManagerGlobal.ADD_PERMISSION_DENIED: throw new WindowManager.BadTokenException("Unable to add window " + mWindow + " -- permission denied for window type " + mWindowAttributes.type); case WindowManagerGlobal.ADD_INVALID_DISPLAY: throw new WindowManager.InvalidDisplayException("Unable to add window " + mWindow + " -- the specified display can not be found"); case WindowManagerGlobal.ADD_INVALID_TYPE: throw new WindowManager.InvalidDisplayException("Unable to add window " + mWindow + " -- the specified window type " + mWindowAttributes.type + " is not valid"); } throw new RuntimeException( "Unable to add window -- unknown error code " + res); } // Omit some codes } } }
Keep looking
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) { return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outStableInsets, outOutsets, outInputChannel); }
com.android.server.wm.WindowManagerService#addWindow
public int addWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) { // Omit some codes synchronized(mWindowMap) { // Omit some codes boolean addToken = false; WindowToken token = mTokenMap.get(attrs.token); AppWindowToken atoken = null; boolean addToastWindowRequiresToken = false; if (token == null) { if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) { Slog.w(TAG_WM, "Attempted to add application window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_INPUT_METHOD) { Slog.w(TAG_WM, "Attempted to add input method window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_VOICE_INTERACTION) { Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_WALLPAPER) { Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_DREAM) { Slog.w(TAG_WM, "Attempted to add Dream window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_QS_DIALOG) { Slog.w(TAG_WM, "Attempted to add QS dialog window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_ACCESSIBILITY_OVERLAY) { Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } if (type == TYPE_TOAST) { // Apps targeting SDK above N MR1 cannot arbitrary add toast windows. if (doesAddToastWindowRequireToken(attrs.packageName, callingUid, attachedWindow)) { Slog.w(TAG_WM, "Attempted to add a toast window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } } token = new WindowToken(this, attrs.token, -1, false); addToken = true; } // Omit some codes } // Omit some codes return res; }
Here we should go to the branch where the token is equal to null. As mentioned above, we will add the token to the HashMap. Why is it null here? Let's return to the method of sending delay messages, scheduleTimeoutLocked
private void scheduleTimeoutLocked(ToastRecord r) { mHandler.removeCallbacksAndMessages(r); Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); // Here, the delay time is set according to the passed in duration // Toast.LENGTH_LONG corresponds to LONG_DELAY // Toast.LENGTH_SHORT corresponds to SHORT_DELAY long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; mHandler.sendMessageDelayed(m, delay); } // com.android.server.notification.NotificationManagerService static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT; static final int SHORT_DELAY = 2000; // com.android.server.policy.PhoneWindowManager /** Amount of time (in milliseconds) a toast window can be shown. */ public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds
So you can know LENGTH_LONG display duration 3500 MS length_ The display time of short is 2000 ms, so the above 2000 ms sleep will trigger this delay message
private final class WorkerHandler extends Handler { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_TIMEOUT: handleTimeout((ToastRecord)msg.obj); break; // Omit some codes } } } private void handleTimeout(ToastRecord record) { if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback); synchronized (mToastQueue) { int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } } } void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { // Omit some codes } // Remove current toast ToastRecord lastToast = mToastQueue.remove(index); // The token will be removed here mWindowManagerInternal.removeWindowToken(lastToast.token, true); keepProcessAliveIfNeededLocked(record.pid); // Show next Toast if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so don't assume that the list hasn't changed // after this point. showNextToastLocked(); } } public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.post(mHide); } final Runnable mHide = new Runnable() { @Override public void run() { handleHide(); // Don't do this in handleHide() because it is also invoked by handleShow() mNextView = null; } }; public void handleHide() { if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); if (mView != null) { // note: checking parent() just to make sure the view has // been added... i have seen cases where we get here when // the view isn't yet added, so let's try not to crash. if (mView.getParent() != null) { if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this); // Remove view mWM.removeViewImmediate(mView); } mView = null; } }
Look at the code of removeWindowToken
public void removeWindowToken(IBinder token) { if (!checkCallingPermission(android.Manifest.permission.MANAGE_APP_TOKENS, "removeWindowToken()")) { throw new SecurityException("Requires MANAGE_APP_TOKENS permission"); } final long origId = Binder.clearCallingIdentity(); synchronized(mWindowMap) { DisplayContent displayContent = null; // Remove token WindowToken wtoken = mTokenMap.remove(token); } // Omit some codes }
You can see that the token is removed here, so it is obtained again, so null is returned. To sum up, this exception is thrown when a message blocking in the UI thread causes TN's show method to execute after the hide method. This exception will only occur in Android API 25, because google handled this problem later
Here is the difference between Android API 25 and Android API 26. You can see that this exception is caught directly. This exception cannot be caught directly where the toast pops up, because the exception is thrown in the message loop. There are two ways to repair. One is to replace TN's mHandler through reflection and then add Tay catch to handleMessage. The advantage is that it is relatively simple, but the disadvantage is that the granularity is too coarse. This exception may be caught in other scenarios. The code is as follows:
// Fix android.com on Android API 25 view. WindowManager$BadTokenException private fun fixWindowManagerBadTokenExceptionIn(toast: Toast) { if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1) { return } try { val mTnField = Toast::class.java.getDeclaredField("mTN") mTnField.isAccessible = true val mTn = mTnField.get(toast) val mHandlerField = mTn.javaClass.getDeclaredField("mHandler") mHandlerField.isAccessible = true val mHandler = mHandlerField.get(mTn) as? Handler ?: return mHandlerField.set(mTn, SafelyHandlerWrapper(mHandler)) } catch (e: Exception) { e.printStackTrace() }
The other is to use reflection, but instead of the mContext object in the view, and then obtain various services through the mContext, it will call the getSystemService of the context to judge whether it is context WINDOW_ service can wrap the originally returned WindowManager in a layer, and then try catch the addView method of the WindowManager. The code will not be pasted Github address
2, Toast cannot pop up after some devices close the notification permission
The user may turn off the notification permission of the App. The user's expectation may just want to block push messages, not Toast. When various Toast pop-up needs to be popped up, it will be strange that the user experience is too poor. First analyze the cause of the problem, and then look back
private final IBinder mService = new INotificationManager.Stub() { @Override 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 ; } final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); final boolean isPackageSuspended = isPackageSuspendedForUser(pkg, Binder.getCallingUid()); // When all three conditions are met, the process ends // noteNotificationOp is used to detect whether there is notification permission 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; } } } }
You can see that if the process ends without notification permission, Toast will not be added to the list and will not be displayed. The solutions are as follows:
The first is to avoid the NotificationManagerService handling the display and hiding of view s through WindowManager. The disadvantage is that when there is no floating window permission, it can only be displayed on the current page. The specific implementation is as follows: ToastUtils
The second is to set the Toast view to the dialog's contentView through dialog. The specific implementation is as follows: smart-show The disadvantage of this scheme is that it can only be displayed on the current page
The third is through the specific analysis of Snackbar About Toast and Snackbar Because there is no code, you can't see the specific effect. The disadvantage is that it's too troublesome to use Toast
The fourth method is to add Toast to the list through dynamic proxy
private final IBinder mService = new INotificationManager.Stub() { @Override 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 ; } // The value of isSystemToast depends on isCallerSystem() and "Android" equals(pkg) // Therefore, if the value of pkg is "Android" and issystemtoast is true, it can pass the verification final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg)); final boolean isPackageSuspended = isPackageSuspendedForUser(pkg, Binder.getCallingUid()); // When all three conditions are met, the process ends // noteNotificationOp is used to detect whether there is notification permission if (ENABLE_BLOCKED_TOASTS && (!noteNotificationOp(pkg, Binder.getCallingUid()) || isPackageSuspended)) { // If isSystemToast returns true, it can pass the verification if (!isSystemToast) { Slog.e(TAG, "Suppressing toast from package " + pkg + (isPackageSuspended ? " due to package suspended by administrator." : " by user request.")); return; } } } }
According to the above analysis, just change the value of the first parameter pkg to "true". The specific code is here The notification permission of some Android phones is closed, and Toast cannot be played
Reference and thanks
Tip: the links listed above will not be repeated
WindowManager$BadTokenException(WindowManager source code analysis)