From 0adcce60ad800e5ae302d79ac9de0901d2c0ed9c Mon Sep 17 00:00:00 2001 From: Charles Chen Date: Fri, 23 Apr 2021 21:30:02 +0800 Subject: [PATCH] Fix Accessibility apps crash when adding view by ... AccessibilityService created WindowContext In R, we introduce WindowContext to handle non-activity windows. When developers want to add an accessibility overlay window on secondary display, it is suggested to call createDisplayContext(secondaryDisplay) .createWindowContext(TYPE_ACCESSIBILITY_OVERLAY, null) or call createWindowContext(secondaryDisplay, TYPE_ACCESSIBILITY_OVERLAY, null) in S. However, it doesn't work now because the WindowToken with type TYPE_ACCESSIBILITY_OVERLAY is managed by system server, and Accessibility app should obtain the WindowToken to attach the overlay window. The current mechanism of AccessibilityService is to set default token to WindowManager for created DisplayContext. This CL follows this mechanism on WindowContext to set the default token. Test: atest AccessibilityServiceTest fixes: 186085801 Change-Id: I2ac2cf1c72adebf33eb6b6883437a7f13993d8a9 --- .../AccessibilityService.java | 97 +++++++++++--- .../AccessibilityServiceTest.java | 126 +++++++++++++++++- 2 files changed, 202 insertions(+), 21 deletions(-) diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 483defab43c90..0a4e822c8c7d5 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -16,6 +16,8 @@ package android.accessibilityservice; +import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; + import android.accessibilityservice.GestureDescription.MotionEventGenerator; import android.annotation.CallbackExecutor; import android.annotation.ColorInt; @@ -27,6 +29,7 @@ import android.annotation.TestApi; import android.app.Service; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; import android.content.pm.ParceledListSlice; import android.graphics.Bitmap; @@ -36,6 +39,7 @@ import android.graphics.Region; import android.hardware.HardwareBuffer; import android.hardware.display.DisplayManager; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -961,30 +965,31 @@ public abstract class AccessibilityService extends Service { } } + @NonNull @Override public Context createDisplayContext(Display display) { - final Context context = super.createDisplayContext(display); - final int displayId = display.getDisplayId(); - setDefaultTokenInternal(context, displayId); - return context; + return new AccessibilityContext(super.createDisplayContext(display), mConnectionId); } - private void setDefaultTokenInternal(Context context, int displayId) { - final WindowManagerImpl wm = (WindowManagerImpl) context.getSystemService(WINDOW_SERVICE); - final IAccessibilityServiceConnection connection = - AccessibilityInteractionClient.getInstance(this).getConnection(mConnectionId); - IBinder token = null; - if (connection != null) { - synchronized (mLock) { - try { - token = connection.getOverlayWindowToken(displayId); - } catch (RemoteException re) { - Log.w(LOG_TAG, "Failed to get window token", re); - re.rethrowFromSystemServer(); - } - } - wm.setDefaultToken(token); + @NonNull + @Override + public Context createWindowContext(int type, @Nullable Bundle options) { + final Context context = super.createWindowContext(type, options); + if (type != TYPE_ACCESSIBILITY_OVERLAY) { + return context; } + return new AccessibilityContext(context, mConnectionId); + } + + @NonNull + @Override + public Context createWindowContext(@NonNull Display display, int type, + @Nullable Bundle options) { + final Context context = super.createWindowContext(display, type, options); + if (type != TYPE_ACCESSIBILITY_OVERLAY) { + return context; + } + return new AccessibilityContext(context, mConnectionId); } /** @@ -2675,4 +2680,58 @@ public abstract class AccessibilityService extends Service { } } } + + private static class AccessibilityContext extends ContextWrapper { + private final int mConnectionId; + + private AccessibilityContext(Context base, int connectionId) { + super(base); + mConnectionId = connectionId; + setDefaultTokenInternal(this, getDisplayId()); + } + + @NonNull + @Override + public Context createDisplayContext(Display display) { + return new AccessibilityContext(super.createDisplayContext(display), mConnectionId); + } + + @NonNull + @Override + public Context createWindowContext(int type, @Nullable Bundle options) { + final Context context = super.createWindowContext(type, options); + if (type != TYPE_ACCESSIBILITY_OVERLAY) { + return context; + } + return new AccessibilityContext(context, mConnectionId); + } + + @NonNull + @Override + public Context createWindowContext(@NonNull Display display, int type, + @Nullable Bundle options) { + final Context context = super.createWindowContext(display, type, options); + if (type != TYPE_ACCESSIBILITY_OVERLAY) { + return context; + } + return new AccessibilityContext(context, mConnectionId); + } + + private void setDefaultTokenInternal(Context context, int displayId) { + final WindowManagerImpl wm = (WindowManagerImpl) context.getSystemService( + WINDOW_SERVICE); + final IAccessibilityServiceConnection connection = + AccessibilityInteractionClient.getConnection(mConnectionId); + IBinder token = null; + if (connection != null) { + try { + token = connection.getOverlayWindowToken(displayId); + } catch (RemoteException re) { + Log.w(LOG_TAG, "Failed to get window token", re); + re.rethrowFromSystemServer(); + } + wm.setDefaultToken(token); + } + } + } } diff --git a/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java b/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java index c65ef9a56cd89..53ba140e6aad7 100644 --- a/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java +++ b/core/tests/coretests/src/android/accessibilityservice/AccessibilityServiceTest.java @@ -16,18 +16,40 @@ package android.accessibilityservice; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; +import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; +import android.content.Context; import android.content.Intent; +import android.graphics.PixelFormat; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.ImageReader; +import android.os.Binder; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; +import android.util.SparseArray; +import android.view.Display; +import android.view.View; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; +import android.window.WindowTokenClient; import androidx.test.InstrumentationRegistry; +import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,6 +64,8 @@ import org.mockito.MockitoAnnotations; public class AccessibilityServiceTest { private static final String TAG = "AccessibilityServiceTest"; private static final int CONNECTION_ID = 1; + private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams( + TYPE_ACCESSIBILITY_OVERLAY); private static class AccessibilityServiceTestClass extends AccessibilityService { private IAccessibilityServiceClient mCallback; @@ -49,7 +73,11 @@ public class AccessibilityServiceTest { AccessibilityServiceTestClass() { super(); - attachBaseContext(InstrumentationRegistry.getContext()); + Context context = ApplicationProvider.getApplicationContext(); + final Display display = context.getSystemService(DisplayManager.class) + .getDisplay(DEFAULT_DISPLAY); + + attachBaseContext(context.createTokenContext(new WindowTokenClient(), display)); mLooper = InstrumentationRegistry.getContext().getMainLooper(); } @@ -78,14 +106,33 @@ public class AccessibilityServiceTest { private @Mock IBinder mMockIBinder; private IAccessibilityServiceClient mServiceInterface; private AccessibilityServiceTestClass mService; + private final SparseArray mWindowTokens = new SparseArray<>(); @Before - public void setUp() throws RemoteException { + public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mService = new AccessibilityServiceTestClass(); + mService.onCreate(); mService.setupCallback(mMockClientForCallback); mServiceInterface = (IAccessibilityServiceClient) mService.onBind(new Intent()); mServiceInterface.init(mMockConnection, CONNECTION_ID, mMockIBinder); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + final int displayId = (int) args[0]; + final IBinder token = new Binder(); + WindowManagerGlobal.getWindowManagerService().addWindowToken(token, + TYPE_ACCESSIBILITY_OVERLAY, displayId, null /* options */); + mWindowTokens.put(displayId, token); + return token; + }).when(mMockConnection).getOverlayWindowToken(anyInt()); + } + + @After + public void tearDown() throws Exception { + for (int i = mWindowTokens.size() - 1; i >= 0; --i) { + WindowManagerGlobal.getWindowManagerService().removeWindowToken( + mWindowTokens.valueAt(i), mWindowTokens.keyAt(i)); + } } @Test @@ -101,4 +148,79 @@ public class AccessibilityServiceTest { verify(mMockConnection).getSystemActions(); } + + @Test + public void testAddViewWithA11yServiceDerivedDisplayContext() throws Exception { + try (VirtualDisplaySession session = new VirtualDisplaySession()) { + final Context context = mService.createDisplayContext(session.getDisplay()); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> context.getSystemService(WindowManager.class) + .addView(new View(context), mParams) + ); + } + } + + @Test + public void testAddViewWithA11yServiceDerivedWindowContext() throws Exception { + try (VirtualDisplaySession session = new VirtualDisplaySession()) { + final Context context = mService.createDisplayContext(session.getDisplay()) + .createWindowContext(TYPE_ACCESSIBILITY_OVERLAY, null /* options */); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> context.getSystemService(WindowManager.class) + .addView(new View(context), mParams) + ); + } + } + + @Test + public void testAddViewWithA11yServiceDerivedWindowContextWithDisplay() throws Exception { + try (VirtualDisplaySession session = new VirtualDisplaySession()) { + final Context context = mService.createWindowContext(session.getDisplay(), + TYPE_ACCESSIBILITY_OVERLAY, null /* options */); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> context.getSystemService(WindowManager.class) + .addView(new View(context), mParams) + ); + } + } + + @Test(expected = WindowManager.BadTokenException.class) + public void testAddViewWithA11yServiceDerivedWindowContextWithDifferentType() + throws Exception { + try (VirtualDisplaySession session = new VirtualDisplaySession()) { + final Context context = mService.createWindowContext(session.getDisplay(), + TYPE_APPLICATION_OVERLAY, null /* options */); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> context.getSystemService(WindowManager.class) + .addView(new View(context), mParams) + ); + } + } + + + private static class VirtualDisplaySession implements AutoCloseable { + private final VirtualDisplay mVirtualDisplay; + + VirtualDisplaySession() { + final DisplayManager displayManager = ApplicationProvider.getApplicationContext() + .getSystemService(DisplayManager.class); + final int width = 800; + final int height = 480; + final int density = 160; + ImageReader reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, + 2 /* maxImages */); + mVirtualDisplay = displayManager.createVirtualDisplay( + TAG, width, height, density, reader.getSurface(), + VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); + } + + private Display getDisplay() { + return mVirtualDisplay.getDisplay(); + } + + @Override + public void close() throws Exception { + mVirtualDisplay.release(); + } + } }