Android Advanced: Does the Context corresponding to Dialog have to be an Activity? Detailed analysis from the source code

Android Advanced: Does the Context corresponding to Dialog have to be an Activity? Detailed analysis from the source code

[[419839]]

Preface

When creating a Dialog, I know that a context is required in the Dialog constructor, but I don't have a specific concept of this "context", which results in a program error.

Then I found that the only context environment required by Dialog is activity.

So the following article will thoroughly sort out this issue from the perspective of source code;

1. Dialog creation failed

In the Dialog constructor, pass in an Application context. See if the program reports an error:

  1. Dialog dialog = new Dialog(getApplication());
  2. TextView textView = new TextView(this);
  3. textView.setText( "Use Application to create Dialog" );
  4. dialog.setContentView(textView);
  5. dialog.show();

Run the program, and it crashes as expected. Let's take a look at the error message:

  1. Caused by : android. view .WindowManager$BadTokenException: Unable to   add window -- token null is not for an application  
  2. at android.view.ViewRootImpl.setView ( ViewRootImpl.java:517)
  3. at android.view.WindowManagerImpl.addView ( WindowManagerImpl.java:301)
  4. at android.view.WindowManagerImpl.addView ( WindowManagerImpl.java:215)
  5. at android. view .WindowManagerImpl$CompatModeWrapper.addView(WindowManagerImpl.java:140)

There are two points we need to pay attention to in this error log

  • The program reported a BadTokenException;
  • The program error is in the setView method of ViewRootImpl;
  • We must be wondering what BadTokenException is. Before explaining this, we first need to understand Token. After understanding the concept of Token, combined with the setView method of ViewRootImpl, we can understand what BadTokenException is and how it is generated.

Token Analysis

1. Token details

Token literally means token in Chinese. It is used as a security mechanism in the Android system. Its essence is a Binder object, which acts as a verification code in the process of cross-process communication. For example, in the process of starting the activity and drawing the interface, the communication between the three processes of ActivityManagerService, application, and WindowManagerService will be involved. At this time, Token acts as an authentication function in these three processes. ActivityManagerService and WindowManagerService use the Token passed by the application's activity to distinguish which activity controls the application. Specifically:

  • In the process of starting an Activity, first, ActivityManagerService will create an ActivityRecord to be managed by itself, and at the same time create an IApplication (essentially a Binder) for this ActivityRecord.
  • ActivityManagerService passes this binder object to WindowManagerService, allowing WindowManagerService to record this Binder.
  • When ActivityManagerService completes the addition of the data structure, it will return an ActivityClientRecord data structure to ActivityThread, which contains the Binder object Token.
  • After ActivityThread gets the Binder object of this Token, it needs to ask WindowManagerService to add a corresponding window on the interface. The Token is included in the WindowManager.LayoutParams data passed to WindowManagerService when adding the window.
  • Finally, when WindowManagerService adds a window, it needs to compare the Binder of this Token with the Binder previously saved by ActivityManagerService. If it passes the verification, it means it is legal. Otherwise, it will throw the BadTokenException.
  • At this point, we know what BadTokenException is, and then we will analyze why the Application context will report a BadTokenException, while the Activity context will not.

2. Why do we need a token?

Because WMS needs to use this token to determine the position of the window (not the coordinates). If there is no token, it will not know which container the window should be placed in.

Because the WindowManger of the non-Activity Context does not have a ParentWindow, the corresponding container cannot be found on the WMS side, that is, it does not know where to place the Dialog Window.

Another reason is that there is no SYSTEM_ALERT_WINDOW permission (of course, permissions are required. The subcontainer of DisplayArea.Tokens has a higher level than the Window of ordinary applications, which means it will be displayed in front of the Window of ordinary applications. If permission control is not added, it will be abused).

After obtaining the SYSTEM_ALERT_WINDOW permission and specifying the Window.type of the Dialog as SYSTEM_WINDOW, it can be displayed normally because WMS will create a WindowToken specifically for the SYSTEM_WINDOW type window (now there is a container) and place it in DisplayArea.Tokens (now you know where to put it);

3. Create dialog process analysis

1. The activity interface is finally connected to WindowManagerService through the setView method of ViewRootImpl, so that WindowManagerService draws the interface on the mobile phone screen. From the above exception log, we can also see that the Dialog interface is also connected to WindowManagerService through the setView of ViewRootImpl to complete the drawing of the interface.

Let's first look at the Dialog constructor. Regardless of the one-parameter constructor or the two-parameter constructor, the three-parameter constructor will eventually be called:

  1. Dialog(@NonNull Context context, @StyleRes int themeResId, boolean
  2. createContextThemeWrapper) {
  3. ......
  4. //1. Create a WindowManagerImpl object
  5. mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  6. //2. Create a PhoneWindow object
  7. final Window w = new PhoneWindow(mContext);
  8. mWindow = w;
  9. //3. Enable dialog to respond to user events
  10. w.setCallback(this);
  11. w.setOnWindowDismissedCallback(this);
  12. //4. Set WindowManager for the window object
  13. w.setWindowManager(mWindowManager, null , null );
  14. w.setGravity(Gravity.CENTER);
  15. mListenersHandler = new ListenersHandler(this);
  16. }

From this code, we can see that the creation of a dialog is essentially the same as the creation of an activity interface. Both require the creation of an application window Window and an application window view object manager WindowManagerImpl.

Then Dialog also has a setContentView method:

  1. public void setContentView(@LayoutRes int layoutResID) {
  2. mWindow.setContentView(layoutResID);
  3. }
  4. Still call the setContentView method of PhoneWindow. Next, let's look at the show method of dialog:
  5. public void show() {
  6. ......
  7. //1. Get the DecorView encapsulated by the setView method
  8. mDecor = mWindow.getDecorView();
  9. ......
  10. //2. Get the member variable WindowManager.LayoutParams that has been initialized when PhoneWindow is created
  11. WindowManager.LayoutParams l = mWindow.getAttributes();
  12. if ((l.softInputMode
  13. & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
  14. WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
  15. nl.copyFrom(l);
  16. nl.softInputMode |=
  17. WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
  18. l = nl;
  19. }
  20. try {
  21. //3. Add DecorView to the screen through WindowManagerImpl
  22. mWindowManager.addView(mDecor, l);
  23. mShowing = true ;
  24. sendShowMessage();
  25. finally
  26. }
  27. }

This code is similar to the makeVisible method of activity, so I won't go into details here. The comments are enough to make it clear. Then call the addView method of WindowManagerImpl:

  1. @Override
  2. public void addView(@NonNull View   view , @NonNull ViewGroup.LayoutParams params) {
  3. applyDefaultToken(params);
  4. mGlobal.addView( view , params, mDisplay, mParentWindow);
  5. }
  6. Then the addView method of WindowManagerGlobal is called:
  7. public void addView( View   view , ViewGroup.LayoutParams params,
  8. Display display, Window parentWindow) {
  9. ......
  10. //1. Convert the incoming ViewGroup.LayoutParams type params into
  11. wparams of type WindowManager.LayoutParams
  12. final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)
  13. params;
  14. //2. If WindowManagerImpl is created in the activity method, it is not empty
  15. if (parentWindow != null ) {
  16. parentWindow.adjustLayoutParamsForSubWindow(wparams);
  17. } else {
  18. ......
  19. }
  20. ViewRootImpl root;
  21. View panelParentView = null ;
  22. synchronized (mLock) {
  23. ......
  24. root = new ViewRootImpl( view .getContext(), display);
  25. view .setLayoutParams(wparams);
  26. //3. Store the view object view , ViewRootImpl and wparams in the corresponding positions of the corresponding collections respectively
  27. mViews.add ( view );
  28. mRoots.add (root);
  29. mParams.add (wparams);
  30. }
  31. // do this last because it fires off messages to start doing things
  32. try {
  33. //4. Contact WindowManagerService through ViewRootImpl to draw the view to the screen
  34. root.setView( view , wparams, panelParentView);
  35. } catch (RuntimeException e) {
  36. // BadTokenException or InvalidDisplayException, clean up.
  37. synchronized (mLock) {
  38. final int   index = findViewLocked( view , false );
  39. if ( index >= 0) {
  40. removeViewLocked( index , true );
  41. }
  42. }
  43. throw e;
  44. }
  45. }
  1. //2. If WindowManagerImpl is created in the activity method, it is not empty
  2. if (parentWindow != null ) {
  3. parentWindow.adjustLayoutParamsForSubWindow(wparams);
  4. } else {
  5. ......
  6. }

2. Here, we will first determine whether a parentWindow of type Window is empty. If it is not empty, we will adjust some property values ​​of a variable wparams of type WindowManager.LayoutParams through the Window's adjustLayoutParamsForSubWindow method. When an application requests the WindowManagerService service, it will pass in a Token. In fact, that Token will be stored in the token variable of wparams through the Window's adjustLayoutParamsForSubWindow method. That is to say, if the Window's adjustLayoutParamsForSubWindow method is not called, the token variable of wparams will be empty. Then let's take a look at how the token variable of wparams is assigned:

  1. void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
  2. CharSequence curTitle = wp.getTitle();
  3. if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
  4. wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
  5. ......
  6. } else {
  7. if (wp.token == null ) {
  8. wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
  9. }
  10. ......
  11. }
  12. if (wp.packageName == null ) {
  13. wp.packageName = mContext.getPackageName();
  14. }
  15. if (mHardwareAccelerated) {
  16. wp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
  17. }

Here we can see that this code will first make a judgment if the value of wp.type is between WindowManager.LayoutParams.FIRST_SUB_WINDOW and WindowManager.LayoutParams.LAST_SUB_WINDOW. If not, wp.token will be assigned a value. wp.type represents the window type, which has three levels: system level, application level, and sub-window level. Here, it is used to judge whether it is at the sub-window level. The default value of Dialog's WindowManager.LayoutParams.type is application level, so it will take the else branch and assign mAppToken to wp.token. As for what mAppToken is, we will analyze it later.

3. Look at the addView method of WindowManagerGlobal, which calls the setView method of ViewRootImpl. Let's see how ViewRootImpl connects to WindowManagerService to pass token:

  1. public void setView( View   view , WindowManager.LayoutParams attrs, View   
  2. panelParentView) {
  3. synchronized (this) {
  4. if (mView == null ) {
  5. mView = view ;
  6. try {
  7. ......
  8. //1. Call the WindowManagerService interface request through the binder object mWindowSession
  9. res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
  10. getHostVisibility(), mDisplay.getDisplayId(),
  11. mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
  12. mAttachInfo.mOutsets, mInputChannel);
  13. } catch (RemoteException e) {
  14. ......
  15. throw new RuntimeException( "Adding window failed" , e);
  16. finally
  17. if (restore) {
  18. attrs.restore();
  19. }
  20. }
  21. ......
  22. if (res < WindowManagerGlobal.ADD_OKAY) {
  23. ......
  24. switch (res) {
  25. case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
  26. case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
  27. throw new WindowManager.BadTokenException(
  28. "Unable to add window -- token " + attrs.token
  29. + " is not valid; is your activity running?" );
  30. //2. If the request fails (token verification fails), a BadTokenException is thrown
  31. case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
  32. throw new WindowManager.BadTokenException(
  33. "Unable to add window -- token " + attrs.token
  34. + " is not for an application" );
  35. case WindowManagerGlobal.ADD_APP_EXITING:
  36. throw new WindowManager.BadTokenException(
  37. "Unable to add window -- app for token " +
  38. attrs.token
  39. + " is exiting" );
  40. case WindowManagerGlobal.ADD_DUPLICATE_ADD:
  41. throw new WindowManager.BadTokenException(
  42. "Unable to add window -- window " + mWindow
  43. + " has already been added" );
  44. case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
  45. // Silently ignore   -- we would have just removed it  
  46. // right away, anyway.
  47. return ;
  48. case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
  49. throw new WindowManager.BadTokenException(
  50. "Unable to add window " + mWindow +
  51. " -- another window of this type already  
  52. exists");
  53. case WindowManagerGlobal.ADD_PERMISSION_DENIED:
  54. throw new WindowManager.BadTokenException(
  55. "Unable to add window " + mWindow +
  56. " -- permission denied for this window type" );
  57. case WindowManagerGlobal.ADD_INVALID_DISPLAY:
  58. throw new WindowManager.InvalidDisplayException(
  59. "Unable to add window " + mWindow +
  60. " -- the specified display can not be found" );
  61. case WindowManagerGlobal.ADD_INVALID_TYPE:
  62. throw new WindowManager.InvalidDisplayException(
  63. "Unable to add window " + mWindow
  64. + " -- the specified window type is not valid" );
  65. }
  66. throw new RuntimeException(
  67. "Unable to add window -- unknown error code " + res);
  68. }
  69. ......
  70. }
  71. }
  72. }

There are two things to note about this code:

  • The WindowManagerService service is requested through a binder object of mWindowSession, and a variable mWindowAttributes of type WindowManager.LayoutParams is passed to WindowManagerService, which contains a token object representing the current activity. Then the screen view is created through the WindowManagerService service.
  • The success of the request will be determined based on the return result of the request to the WindowManagerService service. If the request fails, an exception will be thrown. The comment is the exception thrown in the example at the beginning of the article. At this time, attrs.token is empty. Why is it not empty if the context of creating the dialog is changed to activity?

4. Analyze why the context Activity for creating Dialog is different

1. From the above analysis, we can see that attrs.token is assigned in the adjustLayoutParamsForSubWindow method of Window. The default WindowManager.LayoutParams.type of Dialog is at the application level. Therefore, if you can enter this method, attrs.token will definitely be assigned. Now there is only one situation. If it is not the context of activity, you will not enter this method. At this time, let's look at the addView method of WindowManagerGlobal:

  1. public void addView( View   view , ViewGroup.LayoutParams params,
  2. Display display, Window parentWindow) {
  3. ......
  4. //2. If WindowManagerImpl is created in the activity method, it is not empty
  5. if (parentWindow != null ) {
  6. parentWindow.adjustLayoutParamsForSubWindow(wparams);
  7. } else {
  8. ......
  9. }
  10. ......
  11. }

From here, we can see that if the parentWindow of the Window type is null, the adjustLayoutParamsForSubWindow method will not be entered. Therefore, we can conclude that if the fourth parameter parentWindow of the WindowManagerGlobal of the activity context is not null, then we will analyze why other contexts will cause parentWindow to be null.

WindowManagerGlobal calls the addView method in the addView method of WindowManagerImpl:

  1. @Override
  2. public void addView(@NonNull View   view , @NonNull ViewGroup.LayoutParams params) {
  3. applyDefaultToken(params);
  4. mGlobal.addView( view , params, mDisplay, mParentWindow);
  5. }
  6. The addView method of WindowManagerImpl is called in the first method of Dialog:
  7. public void show() {
  8. ......
  9. try {
  10. mWindowManager.addView(mDecor, l);
  11. mShowing = true ;
  12. sendShowMessage();
  13. finally
  14. }
  15. }

Compare these two methods. It can be seen that the addView method of WindowManagerImpl calls the addView method of WindowManagerGlobal with two additional parameters, mDisplay and mParentWindow. We only look at the latter one, which has an additional Window type mParentWindow. It can be seen that mParentWindow is not assigned in the show method of Dialog. So where is it assigned? Searching for mParentWindow in the WindowManagerImpl class, it is found that it is assigned in the two-parameter constructor of WindowManagerImpl. From this we can guess that if the activity context is used, the two-parameter constructor is used when creating the WindowManagerImpl instance, while other contexts use the one-parameter constructor. Now the question is focused on how WindowManagerImpl is created.

Let's look back at how WindowManagerImpl is created in the Dialog constructor:

  1. Dialog(@NonNull Context context, @StyleRes int themeResId, boolean
  2. createContextThemeWrapper) {
  3. ......
  4. mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  5. ......
  6. }
  7. Then check the getSystemService method of activity and the getSystemService method of Application respectively:
  8. Activity's getSystemService method
  9. @Override
  10. public Object getSystemService(@ServiceName @NonNull String name ) {
  11. ......
  12. if (WINDOW_SERVICE.equals( name )) {
  13. return mWindowManager;
  14. } else if (SEARCH_SERVICE.equals( name )) {
  15. ensureSearchManager();
  16. return mSearchManager;
  17. }
  18. return super.getSystemService( name );
  19. }

In this method, the activity's mWindowManager object is directly returned. The activity's mWindowManager object is in the activity's attach method:

  1. final void attach(Context context, ActivityThread aThread,
  2. Instrumentation instr, IBinder token, int ident,
  3. Application application, Intent intent, ActivityInfo info,
  4. CharSequence title, Activity parent, String id,
  5. NonConfigurationInstances lastNonConfigurationInstances,
  6. Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
  7. ......
  8. mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),mToken, mComponent.flattenToString(),
  9. (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
  10. ......
  11. }

2. Let's look at the setWindowManager method of Window:

  1. public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
  2. boolean hardwareAccelerated) {
  3. //1. Save the Token passed by ActivityManagerService to mAppToken
  4. mAppToken = appToken;
  5. //2. Create WindowManagerImpl
  6. mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
  7. }

There are two places to note in this code. First, the Token passed by ActivityManagerService is assigned to Winow's mAppToken. This token will eventually be saved to attr.token. The specific operation is in Window's adjustLayoutParamsForSubWindow method. Second, the createLocalWindowManager method of WindowManagerImpl is called to create WindowManagerImpl:

  1. public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
  2. return new WindowManagerImpl(mDisplay, parentWindow);
  3. }

It can be seen here that if the context of creating Dialog is activity, the constructor of WindowManagerImpl with two parameters will be called, resulting in parentWindow not being empty.

3. Application's getSystemService method:

Since Application is a subclass of Context, the getSystemService of Application will eventually be transferred to the getSystemService method of ContextImpl

  1. @Override
  2. public Object getSystemService(String name ) {
  3. return SystemServiceRegistry.getSystemService(this, name );
  4. }
  5. The getSystemService method of SystemServiceRegistry is called directly, and this method will get the return value of the createService method of the anonymous inner class CachedServiceFetcher<WindowManager>.
  6. @Override
  7. public WindowManager createService(ContextImpl ctx) {
  8. return new WindowManagerImpl(ctx.getDisplay());
  9. }});

From this method, we can see that when the context is Application, the constructor of a parameter of WindowManagerImpl is called, so parentWindow is empty;

Summarize

  • When creating a dialog, if the context passed into the constructor is not an activity type, the variable mParentWindow of WindowManagerImpl type is Window, which causes WindowManagerGlobal's addView to not call Window's adjustLayoutParamsForSubWindow method, and thus does not assign a value to attr.token, causing authentication failure in the WindowManagerService service and throwing a BadTokenException exception;
  • To show a normal Dialog, you need a container token instead of the Activity itself. We usually pass the Activity, but the Activity just happens to correspond to a WindowState container on the WMS side.

This article is reproduced from the WeChat public account "Android Development Programming"

<<:  After studying more than 1,000 cases, it was found that user experience design can be refined into these 12 steps

>>:  Jamf releases iOS app permissions report and recommends users block access to non-essential permissions

Recommend

Can multi-screen interaction on smart TVs really improve user experience?

Multi-screen interaction centered on TV has gradu...

What is a leap year? Why is there an extra day every 4 years?

We usually celebrate our birthday once a year. Fo...

Why does our brain go “blank” when it comes to important moments?

Audit expert: Yin Tielun Deputy Chief Physician, ...