From fc7a1b5b34ad15bee492deab8123d7a548c8b8b3 Mon Sep 17 00:00:00 2001 From: Charles Chen Date: Wed, 16 Jun 2021 20:26:34 +0800 Subject: [PATCH 1/2] Migrate InputMethodService to WindowProviderService Also introduce IWindowManager#getDisplayIdToLaunchIme to make IMS be aware of the launched display to prevent extra onConfigurationChanged callback Bug: 149463653 Test: atest MultiDisplaySystemDecorationTests CtsInputMethodTestCases Test: atest ContextTest ContextIsUiContextTest Test: manual - moving IME between 2 displays and displayArea within display - config change received Test: manual - the app to show IME crashed and focus is set to the next task - no config change Change-Id: Ie565e30ed5dd3f2cfe27355a6dded76dc3adc14b --- core/api/test-current.txt | 7 +++ core/java/android/app/ContextImpl.java | 6 --- .../AbstractInputMethodService.java | 50 +++++++++++++++++-- .../InputMethodService.java | 3 ++ core/java/android/view/IWindowManager.aidl | 7 +++ .../android/window/WindowProviderService.java | 22 ++++++-- .../src/android/content/ContextTest.java | 8 --- .../server/wm/WindowManagerService.java | 20 ++++++++ 8 files changed, 101 insertions(+), 22 deletions(-) diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 42708f9d2e158..23926a3c042e6 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1270,6 +1270,12 @@ package android.hardware.soundtrigger { package android.inputmethodservice { + public abstract class AbstractInputMethodService extends android.window.WindowProviderService implements android.view.KeyEvent.Callback { + method public final int getInitialDisplayId(); + method @Nullable public final android.os.Bundle getWindowContextOptions(); + method public final int getWindowType(); + } + @UiContext public class InputMethodService extends android.inputmethodservice.AbstractInputMethodService { field public static final long FINISH_INPUT_NO_FALLBACK_CONNECTION = 156215187L; // 0x94fa793L } @@ -3242,6 +3248,7 @@ package android.window { @UiContext public abstract class WindowProviderService extends android.app.Service { ctor public WindowProviderService(); method public final void attachToWindowToken(@NonNull android.os.IBinder); + method @NonNull public int getInitialDisplayId(); method @Nullable public android.os.Bundle getWindowContextOptions(); method public abstract int getWindowType(); } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 16b6ea5bcf42b..c42c38be0ae82 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -3194,12 +3194,6 @@ class ContextImpl extends Context { @UnsupportedAppUsage final void setOuterContext(@NonNull Context context) { mOuterContext = context; - // TODO(b/149463653): check if we still need this method after migrating IMS to - // WindowContext. - if (mOuterContext.isUiContext() && mContextType <= CONTEXT_TYPE_DISPLAY_CONTEXT) { - mContextType = CONTEXT_TYPE_WINDOW_CONTEXT; - mIsConfigurationBasedContext = true; - } } @UnsupportedAppUsage diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java index 3cd13a212a4b8..17d4ae6205caa 100644 --- a/core/java/android/inputmethodservice/AbstractInputMethodService.java +++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java @@ -18,16 +18,23 @@ package android.inputmethodservice; import android.annotation.MainThread; import android.annotation.NonNull; -import android.app.Service; +import android.annotation.Nullable; +import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; import android.os.IBinder; +import android.os.RemoteException; import android.util.proto.ProtoOutputStream; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodSession; +import android.window.WindowProviderService; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -44,9 +51,22 @@ import java.io.PrintWriter; * implement. This base class takes care of reporting your InputMethod from * the service when clients bind to it, but provides no standard implementation * of the InputMethod interface itself. Derived classes must implement that - * interface. + * interface.

+ * + *

After {@link android.os.Build.VERSION_CODES#S}, the maximum possible area to show the soft + * input may not be the entire screen. For example, some devices may support to show the soft input + * on only half of screen.

+ * + *

In that case, moving the soft input from one half screen to another will trigger a + * {@link android.content.res.Resources} update to match the new {@link Configuration} and + * this {@link AbstractInputMethodService} may also receive a + * {@link #onConfigurationChanged(Configuration)} callback if there's notable configuration changes + *

+ * + * @see android.content.ComponentCallbacks#onConfigurationChanged(Configuration) + * @see Context#isUiContext Context#isUiContext to see the concept of UI Context. */ -public abstract class AbstractInputMethodService extends Service +public abstract class AbstractInputMethodService extends WindowProviderService implements KeyEvent.Callback { private InputMethod mInputMethod; @@ -272,9 +292,33 @@ public abstract class AbstractInputMethodService extends Service public void notifyUserActionIfNecessary() { } + // TODO(b/149463653): remove it in T. We missed the API deadline in S. /** @hide */ @Override public final boolean isUiContext() { return true; } + + /** @hide */ + @Override + public final int getWindowType() { + return WindowManager.LayoutParams.TYPE_INPUT_METHOD; + } + + /** @hide */ + @Override + @Nullable + public final Bundle getWindowContextOptions() { + return null; + } + + /** @hide */ + @Override + public final int getInitialDisplayId() { + try { + return WindowManagerGlobal.getWindowManagerService().getImeDisplayId(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 881e0cfb58d7d..18c6256dff80f 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -629,10 +629,13 @@ public class InputMethodService extends AbstractInputMethodService { throw new IllegalStateException( "attachToken() must be called at most once. token=" + token); } + attachToWindowToken(token); mToken = token; mWindow.setToken(token); } + // TODO(b/149463653): remove updateInputMethodDisplay(int displayId) since we'll get the + // right display by attachToWindowToken /** * {@inheritDoc} * @hide diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 802163617b3b9..305cb97dbc775 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -865,4 +865,11 @@ interface IWindowManager void unregisterCrossWindowBlurEnabledListener(ICrossWindowBlurEnabledListener listener); boolean isTaskSnapshotSupported(); + + /** + * Returns the preferred display ID to show software keyboard. + * + * @see android.window.WindowProviderService#getLaunchedDisplayId + */ + int getImeDisplayId(); } diff --git a/core/java/android/window/WindowProviderService.java b/core/java/android/window/WindowProviderService.java index b8619fbcf3348..c861dbd89e15b 100644 --- a/core/java/android/window/WindowProviderService.java +++ b/core/java/android/window/WindowProviderService.java @@ -36,7 +36,6 @@ import android.view.WindowManager; import android.view.WindowManager.LayoutParams.WindowType; import android.view.WindowManagerImpl; -// TODO(b/159767464): handle #onConfigurationChanged(Configuration) /** * A {@link Service} responsible for showing a non-activity window, such as software keyboards or * accessibility overlay windows. This {@link Service} has similar behavior to @@ -89,6 +88,20 @@ public abstract class WindowProviderService extends Service { return null; } + /** + * Returns the display ID to launch this {@link WindowProviderService}. + * + * @hide + */ + @TestApi + @SuppressLint({"OnNameExpected"}) + // Suppress the lint because it is not a callback and users may override this API to provide + // display. + @NonNull + public int getInitialDisplayId() { + return DEFAULT_DISPLAY; + } + /** * Attaches this WindowProviderService to the {@code windowToken}. * @@ -104,10 +117,9 @@ public abstract class WindowProviderService extends Service { public final Context createServiceBaseContext(ActivityThread mainThread, LoadedApk packageInfo) { final Context context = super.createServiceBaseContext(mainThread, packageInfo); - // Always associate with the default display at initialization. - final Display defaultDisplay = context.getSystemService(DisplayManager.class) - .getDisplay(DEFAULT_DISPLAY); - return context.createTokenContext(mWindowToken, defaultDisplay); + final Display display = context.getSystemService(DisplayManager.class) + .getDisplay(getInitialDisplayId()); + return context.createTokenContext(mWindowToken, display); } @CallSuper diff --git a/core/tests/coretests/src/android/content/ContextTest.java b/core/tests/coretests/src/android/content/ContextTest.java index d1776fb7e5c12..3d7d807ca53d2 100644 --- a/core/tests/coretests/src/android/content/ContextTest.java +++ b/core/tests/coretests/src/android/content/ContextTest.java @@ -32,7 +32,6 @@ import android.content.res.Configuration; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; -import android.inputmethodservice.InputMethodService; import android.media.ImageReader; import android.os.UserHandle; import android.view.Display; @@ -139,13 +138,6 @@ public class ContextTest { assertThat(systemUiContext.isUiContext()).isTrue(); } - @Test - public void testIsUiContext_InputMethodService_returnsTrue() { - final InputMethodService ims = new InputMethodService(); - - assertTrue(ims.isUiContext()); - } - @Test public void testGetDisplayFromDisplayContextDerivedContextOnPrimaryDisplay() { verifyGetDisplayFromDisplayContextDerivedContext(false /* onSecondaryDisplay */); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 339134486b0aa..ff99920a4b9ad 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -46,6 +46,7 @@ import static android.provider.Settings.Global.DEVELOPMENT_WM_DISPLAY_SETTINGS_P import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; +import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; @@ -8632,4 +8633,23 @@ public class WindowManagerService extends IWindowManager.Stub return snapshot != null && snapshot.hasImeSurface(); } } + + @Override + public int getImeDisplayId() { + // TODO(b/189805422): Add a toast to notify users that IMS may get extra + // onConfigurationChanged callback when perDisplayFocus is enabled. + // Enabling perDisplayFocus means that we track focus on each display, so we don't have + // the "top focus" display and getTopFocusedDisplayContent returns the default display + // as the fallback. It leads to InputMethodService receives an extra onConfiguration + // callback when InputMethodService move from a secondary display to another display + // with the same display metrics because InputMethodService will always associate with + // the ImeContainer on the default display in onCreate and receive a configuration update + // to match default display ImeContainer and then receive another configuration update + // from attachToWindowToken. + synchronized (mGlobalLock) { + final DisplayContent dc = mRoot.getTopFocusedDisplayContent(); + return dc.getImePolicy() == DISPLAY_IME_POLICY_LOCAL ? dc.getDisplayId() + : DEFAULT_DISPLAY; + } + } } From 79cb3c52888096eb9f4522f73158d29daa6c156b Mon Sep 17 00:00:00 2001 From: Charles Chen Date: Wed, 16 Jun 2021 20:28:35 +0800 Subject: [PATCH 2/2] Move IME log to WindowTokenClient IMF has the requirement to debug if the configuration is dispatched to InputMethodService. Since we migrated InputMethodService to WindowProviderService, we should also move the debug logic to WindowContext module because config updates will be no longer from ActivityThread. Test: build & run Bug: 135719017 Change-Id: I248c90f36249a6a4537228f7b0204f0d4466ebc7 --- core/java/android/app/ActivityThread.java | 15 ------------ .../android/app/ActivityThreadInternal.java | 2 -- .../android/app/ConfigurationController.java | 21 ----------------- .../android/window/WindowTokenClient.java | 23 +++++++++++++++++++ 4 files changed, 23 insertions(+), 38 deletions(-) diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 3de9a5b1252cb..8662bb4b5dbb0 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -96,7 +96,6 @@ import android.graphics.Canvas; import android.graphics.HardwareRenderer; import android.graphics.Typeface; import android.hardware.display.DisplayManagerGlobal; -import android.inputmethodservice.InputMethodService; import android.media.MediaFrameworkInitializer; import android.media.MediaFrameworkPlatformInitializer; import android.media.MediaServiceManager; @@ -5779,9 +5778,6 @@ public final class ActivityThread extends ClientTransactionHandler final int NSVC = mServices.size(); for (int i=0; i { synchronized (mResourcesManager) { - // TODO (b/135719017): Temporary log for debugging IME service. - if (Build.IS_DEBUGGABLE && mHasImeComponent) { - Log.d(TAG, "ViewRootImpl.ConfigChangedCallback for IME, " - + "config=" + globalConfig); - } - // We need to apply this change to the resources immediately, because upon returning // the view hierarchy will be informed about it. if (mResourcesManager.applyConfigurationToResources(globalConfig, @@ -7884,11 +7874,6 @@ public final class ActivityThread extends ClientTransactionHandler return mDensityCompatMode; } - @Override - public boolean hasImeComponent() { - return mHasImeComponent; - } - // ------------------ Regular JNI ------------------------ private native void nPurgePendingResources(); private native void nDumpGraphicsInfo(FileDescriptor fd); diff --git a/core/java/android/app/ActivityThreadInternal.java b/core/java/android/app/ActivityThreadInternal.java index bc471c4dca187..bc698f6573055 100644 --- a/core/java/android/app/ActivityThreadInternal.java +++ b/core/java/android/app/ActivityThreadInternal.java @@ -32,8 +32,6 @@ interface ActivityThreadInternal { boolean isInDensityCompatMode(); - boolean hasImeComponent(); - boolean isCachedProcessState(); Application getApplication(); diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java index c7702663ec9b9..8637e31eb122c 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -28,12 +28,9 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; -import android.inputmethodservice.InputMethodService; -import android.os.Build; import android.os.LocaleList; import android.os.Trace; import android.util.DisplayMetrics; -import android.util.Log; import android.util.Slog; import android.view.ContextThemeWrapper; import android.view.WindowManagerGlobal; @@ -168,12 +165,7 @@ class ConfigurationController { mPendingConfiguration = null; } - final boolean hasIme = mActivityThread.hasImeComponent(); if (config == null) { - // TODO (b/135719017): Temporary log for debugging IME service. - if (Build.IS_DEBUGGABLE && hasIme) { - Log.w(TAG, "handleConfigurationChanged for IME app but config is null"); - } return; } @@ -204,12 +196,6 @@ class ConfigurationController { mConfiguration = new Configuration(); } if (!mConfiguration.isOtherSeqNewer(config) && compat == null) { - // TODO (b/135719017): Temporary log for debugging IME service. - if (Build.IS_DEBUGGABLE && hasIme) { - Log.w(TAG, "handleConfigurationChanged for IME app but config seq is obsolete " - + ", config=" + config - + ", mConfiguration=" + mConfiguration); - } return; } @@ -237,13 +223,6 @@ class ConfigurationController { ComponentCallbacks2 cb = callbacks.get(i); if (!equivalent) { performConfigurationChanged(cb, config); - } else { - // TODO (b/135719017): Temporary log for debugging IME service. - if (Build.IS_DEBUGGABLE && cb instanceof InputMethodService) { - Log.w(TAG, "performConfigurationChanged didn't callback to IME " - + ", configDiff=" + configDiff - + ", mConfiguration=" + mConfiguration); - } } } } diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index 04ac044aeff10..c32a268cef5c0 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -26,8 +26,11 @@ import android.app.IWindowToken; import android.app.ResourcesManager; import android.content.Context; import android.content.res.Configuration; +import android.inputmethodservice.AbstractInputMethodService; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.util.Log; import java.lang.ref.WeakReference; @@ -43,6 +46,8 @@ import java.lang.ref.WeakReference; * @hide */ public class WindowTokenClient extends IWindowToken.Stub { + private static final String TAG = WindowTokenClient.class.getSimpleName(); + /** * Attached {@link Context} for this window token to update configuration and resources. * Initialized by {@link #attachContext(Context)}. @@ -53,6 +58,8 @@ public class WindowTokenClient extends IWindowToken.Stub { private final Configuration mConfiguration = new Configuration(); + private boolean mShouldDumpConfigForIme; + /** * Attaches {@code context} to this {@link WindowTokenClient}. Each {@link WindowTokenClient} * can only attach one {@link Context}. @@ -69,6 +76,8 @@ public class WindowTokenClient extends IWindowToken.Stub { } mContextRef = new WeakReference<>(context); mConfiguration.setTo(context.getResources().getConfiguration()); + mShouldDumpConfigForIme = Build.IS_DEBUGGABLE + && context instanceof AbstractInputMethodService; } @Override @@ -82,6 +91,13 @@ public class WindowTokenClient extends IWindowToken.Stub { newConfig, newConfig /* overrideConfig */, displayChanged, null /* configChanged */); + if (!shouldUpdateResources && mShouldDumpConfigForIme) { + Log.d(TAG, "Configuration not dispatch to IME because configuration is up" + + " to date. Current config=" + context.getResources().getConfiguration() + + ", reported config=" + mConfiguration + + ", updated config=" + newConfig); + } + if (shouldUpdateResources) { // TODO(ag/9789103): update resource manager logic to track non-activity tokens mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId); @@ -104,6 +120,13 @@ public class WindowTokenClient extends IWindowToken.Stub { ((WindowProviderService) context).onConfigurationChanged(newConfig)); } freeTextLayoutCachesIfNeeded(diff); + if (diff == 0 && mShouldDumpConfigForIme) { + Log.d(TAG, "Configuration not dispatch to IME because configuration has no " + + " public difference with updated config. " + + " Current config=" + context.getResources().getConfiguration() + + ", reported config=" + mConfiguration + + ", updated config=" + newConfig); + } mConfiguration.setTo(newConfig); } if (displayChanged) {