Reference documentation
- Android KeyEvent Click event distribution process
- InputManagerService principle
- Android source code input system window correlation _ Hong Wei column -CSDN blog
The problem background
The company makes an app that pops up a Dialog when the Back button is clicked. Dailog has three buttons. Confirm, upload, cancel. The point is that the app blocks the Back button, and you can only exit the app by clicking the cancel button on Dialog. But in Android 10, if you hit the Back button too often, you go straight back to the desktop.
The normal flow would be, the user hits the Back button, it pops up the Dialog popover, it clicks the Dialog button, it closes the popover, and then it clicks back, it pops up the Dialog button, and so on, without going straight to the desktop.
The sample code
You can directly paste the code below to verify on Android 9 and 10 (11). Frequent clicks on the Back button will result in a straight back to the desktop.
package com.chl.onkeyanalysis;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
AlertDialog alertDialog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getBaseContext();
createAlertDialog();
}
@Nullable
@Override
public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
return super.onCreateView(name, context, attrs);
}
private void createAlertDialog(a) {
alertDialog =
new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle("ww")
.setMessage("ww")
.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
})
.setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK
&& event.getAction() == KeyEvent.ACTION_DOWN) {
Log.i(TAG, "onCancel1 keyBack down: " );
} else if (keyCode == KeyEvent.KEYCODE_BACK
&& event.getAction() == KeyEvent.ACTION_UP) {
Log.i(TAG, "onCancel1 keyBack up: " );
}
return false;
}
})
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
}
})
.setNeutralButton("Upload".new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.create();
}
/ / code 1
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
alertDialog.show();
}
boolean re = super.onKeyDown(keyCode, event);
return re;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return super.onKeyUp(keyCode, event);
}
@Override
public void onBackPressed(a) {
super.onBackPressed();
Log.i(TAG, "onBackPressed: ");
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(TAG, "dispatchTouchEvent: ");
boolean re = super.dispatchTouchEvent(ev);
return re;
}
@Override
protected void onDestroy(a) {
Log.i(TAG, "onDestroy: ");
super.onDestroy(); }}Copy the code
Dialog is setCancelable(true). Click on the back key and it will exit.
Note: The onKeyDown code itself is problematic. Since the back key is being intercepted, it should return false (or in the onBackPressed method) so that it doesn’t have the problem of going back to the desktop. Before the company does not know why the code is written like this, the bug is very easy to solve,onKeyDown code directly return false on OK, but the reason or need to find!
Problem analysis
First of all, I was surprised that the company’s previous code was able to successfully intercept the Activity without exiting. Since in general, onKeyDown returns true, shouldn’t it just exit? To analyze this problem, we first need to locate, in general, after the back key is pressed, how does the Activity exit without intercepting the back key? A normal back key press leads to the onKeyUp method, where the onBackPressed method is called.
And onBackPressed is going to do that, and then finish() is going to be called, so I’m not going to analyze the onBackPressed method here, but if you’re interested, you can look at the code yourself.
Now one thing to remember is that in the instanceMainActivity
Once went to theonBackPressed
The method will exit the application.
So, here we go.
Will onKeyUP be called
So in onKeyDown, we’re going to return true, which is essentially telling the AMS that we’re not going to intercept the event, so we’re going to go to the onKeyUp code, and look at the implementation of onKeyUp.
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.ECLAIR) {
// The code will go there
if(keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && ! event.isCanceled()) { onBackPressed();return true; }}return false;
}
Copy the code
Obviously, if the code wants to go to onBackPressed, two conditions event.istracking () and! event.isCanceled().
So let’s put log in onKeyDown.
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
Log.i(TAG, "onKeyUp: event isTracking:"+(event.isTracking())+" isCanceled:"+event.isCanceled());
return super.onKeyUp(keyCode, event);
}
Copy the code
- In 9.0, the result is:
MainActivity: onKeyUp: event isTracking:true isCanceled:true
Copy the code
- 10.0 System, running result:
MainActivity: onKeyUp: event isTracking:true isCanceled:false
Copy the code
In 9.0, the event is apparently intercepted and cancel is called, whereas in 10.0, cancel is not called.
In 9.0, KeyEvent was actually intercepted by AlertDialog, but in 10.0 it was not intercepted. So I’m wondering if there was an asynchronous operation during Dialog creation. (In fact, that’s exactly why!)
Next, start analyzing the AlertDialog creation process.
The AlertDialog is different from other View components in that it is a child Window. The dependent Activity is its parent Window. When the KeyEvent arrives, the child Window has a higher priority for handling the event.
Process of adding AlertDialog to Window (Android 11)
AlertDialog. Show the final call to WindowManagerGlobal. AddView, AlertDialog. Show no analysis of the specific process. AlertDialog. Show – > WindowManagerImpl. AddView – > WindowManagerGlobal. AddView.
Windows ManagerGlobal is the true implementation of The Methods of Windows Manager, which exist only one per Application
WindowManagerImpl
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
Copy the code
WindowManagerGlobal. AddView (the following code only retained the core steps)
@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {... ViewRootImpl root; View panelParentView =null;
synchronized (mLock) {
.....
/ / create a ViewRootImpl ViewRootImpl implements some interface, convenient WindowManagerGlobal to the operation of the view
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// Add the DecorView to mViews
mViews.add(view);
// Store the ViewRootImpl object in the array
mRoots.add(root);
// Store the LayoutParams arguments in an array
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
// The final operation
root.setView(view, wparams, panelParentView, userId);
} catch(RuntimeException e) { ... }}}Copy the code
The Dialog view is not yet displayed, and the most important step is in the root.setview method.
Next, look at the implementation of viewrooTimpl. setView:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
if (mView == null) { mView = view; mAttachInfo.mDisplayState = mDisplay.getState(); mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); mViewLayoutDirectionInitial = mView.getRawLayoutDirection(); mFallbackEventHandler.setView(view); .// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();// Measure and draw the layout
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = newInputChannel(); } mForceDecorViewVisibility = (mWindowAttributes.privateFlags & PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) ! =0;
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
adjustLayoutParamsForCompatibility(mWindowAttributes);
// Code 1 core code
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
setFrame(mTmpFrame);
} catch (RemoteException e) {
...
} finally {
if(restore) { attrs.restore(); }}... }}}Copy the code
Code 1 through mWindowSession. AddToDisplayAsUser window that has been added to the layer. MWindowSession is an IWindowSession object, and IWindowSession is a Binder that assigns values
public ViewRootImpl(Context context, Display display) {
this(context, display, WindowManagerGlobal.getWindowSession(),
false /* useSfChoreographer */);
}
public ViewRootImpl(Context context, Display display, IWindowSession session) {
this(context, display, session, false /* useSfChoreographer */);
}
public ViewRootImpl(Context context, Display display, IWindowSession session,
boolean useSfChoreographer) { mContext = context; mWindowSession = session; mDisplay = display; . }Copy the code
The incoming is WindowManagerGlobal getWindowSession (); While WindowManagerGlobal. GetWindowSession () implementation process is as follows:
@UnsupportedAppUsage
public static IWindowManager getWindowManagerService(a) {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window")); . }returnsWindowManagerService; }}@UnsupportedAppUsage
public static IWindowSession getWindowSession(a) {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try{... IWindowManager windowManager = getWindowManagerService(); sWindowSession = windowManager.openSession(new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) { ValueAnimator.setDurationScale(scale); }}); }catch (RemoteException e) {
throwe.rethrowFromSystemServer(); }}returnsWindowSession; }}Copy the code
GetWindowSession method is obviously calling IWindowManager openSession method, and IWindowManager is also a binder object, IWindowManager. OpenSession its corresponding is WindowManagerService. OpenSession method. The openSession method returns a Session object.
// -------------------------------------------------------------
// IWindowManager API
// -------------------------------------------------------------
@Override
public IWindowSession openSession(IWindowSessionCallback callback) {
return new Session(this, callback);
}
Copy the code
The session.java code is as follows
class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
final WindowManagerService mService;
final IWindowSessionCallback mCallback;
public Session(WindowManagerService service, IWindowSessionCallback callback) { mService = service; mCallback = callback; . }@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
Rect outStableInsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
outContentInsets, outStableInsets, outDisplayCutout, outInputChannel,
outInsetsState, outActiveControls, UserHandle.getUserId(mUid));
}
@Override
public int addToDisplayAsUser(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, int userId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame, outContentInsets, outStableInsets, outDisplayCutout, outInputChannel, outInsetsState, outActiveControls, userId); }...Copy the code
. Obviously, mWindowSession addToDisplayAsUser code calls is WindowManagerService addWindow method.
WindowManagerService. AddWindow code is as follows:
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState, InsetsSourceControl[] outActiveControls,
int requestUserId) {...if (focusChanged) {
displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus,
false /*updateInputWindows*/);
}
// The core code
displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);
ProtoLog.v(WM_DEBUG_ADD_REMOVE, "addWindow: New client %s"
+ ": window=%s Callers=%s", client.asBinder(), win, Debug.getCallers(5));
if(win.isVisibleOrAdding() && displayContent.updateOrientation()) { displayContent.sendNewConfiguration(); } getInsetsSourceControls(win, outActiveControls); . Binder.restoreCallingIdentity(origId);return res;
}
Copy the code
Finally, here’s where Android 9 differs from its predecessors. The updateInputWindowsLw method updates the input window. The current input window is added, and the current input event will also be updated
Android after 10 scheduleUpdateInputWindows method code InputMonitor. Java# scheduleUpdateInputWindows
.private final UpdateInputWindows mUpdateInputWindows = new UpdateInputWindows();
private class UpdateInputWindows implements Runnable {
@Override
public void run(a) {
synchronized (mService.mGlobalLock) {
mUpdateInputWindowsPending = false;
mUpdateInputWindowsNeeded = false;
if (mDisplayRemoved) {
return;
}
// Populate the input window list with information about all of the windows that
// could potentially receive input.
// As an optimization, we could try to prune the list of windows but this turns
// out to be difficult because only the native code knows for sure which window
// currently has touch focus.
// If there's a drag in flight, provide a pseudo-window to catch drag input
final boolean inDrag = mService.mDragDropController.dragDropActiveLocked();
// Add all windows on the default display.mUpdateInputForAllWindowsConsumer.updateInputWindows(inDrag); }}}void updateInputWindowsLw(boolean force) {
if(! force && ! mUpdateInputWindowsNeeded) {return;
}
scheduleUpdateInputWindows();
}
private void scheduleUpdateInputWindows(a) {
if (mDisplayRemoved) {
return;
}
if(! mUpdateInputWindowsPending) { mUpdateInputWindowsPending =true; mHandler.post(mUpdateInputWindows); }}...Copy the code
Android 9 code
void updateInputWindowsLw(boolean force) {
if(! force && ! mUpdateInputWindowsNeeded) {return;
}
mUpdateInputWindowsNeeded = false;
if (false) Slog.d(TAG_WM, ">>>>>> ENTERED updateInputWindowsLw");
// Populate the input window list with information about all of the windows that
// could potentially receive input.
// As an optimization, we could try to prune the list of windows but this turns
// out to be difficult because only the native code knows for sure which window
// currently has touch focus.
// If there's a drag in flight, provide a pseudo-window to catch drag input
final boolean inDrag = mService.mDragDropController.dragDropActiveLocked();
if (inDrag) {
if (DEBUG_DRAG) {
Log.d(TAG_WM, "Inserting drag window");
}
final InputWindowHandle dragWindowHandle =
mService.mDragDropController.getInputWindowHandleLocked();
if(dragWindowHandle ! =null) {
addInputWindowHandle(dragWindowHandle);
} else {
Slog.w(TAG_WM, "Drag is in progress but there is no "
+ "drag window handle."); }}final boolean inPositioning = mService.mTaskPositioningController.isPositioningLocked();
if (inPositioning) {
if (DEBUG_TASK_POSITIONING) {
Log.d(TAG_WM, "Inserting window handle for repositioning");
}
final InputWindowHandle dragWindowHandle =
mService.mTaskPositioningController.getDragWindowHandleLocked();
if(dragWindowHandle ! =null) {
addInputWindowHandle(dragWindowHandle);
} else {
Slog.e(TAG_WM,
"Repositioning is in progress but there is no drag window handle."); }}// Add all windows on the default display.
mUpdateInputForAllWindowsConsumer.updateInputWindows(inDrag);
if (false) Slog.d(TAG_WM, "<<<<<<< EXITED updateInputWindowsLw");
}
Copy the code
Visible, on the Android 9, call InputMonitor $UpdateInputForAllWindowsConsumer# updateInputWindowsLw method is synchronized, as Android to 10 and after it is asynchronous.
The updateInputWindowsLw method sets the Window of the current Dialog to the top level of the input event. (There are some differences between the updateInputWindowsLw method for Android 9 and Android 11.)
Cause analysis,
Because the call to updateInputWindowsLw in Android 11 is asynchronous, the child window is created, but it is not set as the input source. So if you quickly press the press back key and release, you create a child window on onKeyDown, but when you release the Dialog child window is not set to the top level of the input event, so the event is still passed directly to the MainActivity. And then go to onBackPressed and call Finish,MainActivity is destroyed.