diff options
4 files changed, 235 insertions, 60 deletions
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java index a247db2f3310..0f9075b498ae 100644 --- a/core/java/android/inputmethodservice/NavigationBarController.java +++ b/core/java/android/inputmethodservice/NavigationBarController.java @@ -16,7 +16,6 @@ package android.inputmethodservice; -import static android.content.Intent.ACTION_OVERLAY_CHANGED; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import android.animation.ValueAnimator; @@ -24,18 +23,12 @@ import android.annotation.FloatRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.StatusBarManager; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; import android.inputmethodservice.navigationbar.NavigationBarFrame; import android.inputmethodservice.navigationbar.NavigationBarView; -import android.os.PatternMatcher; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -145,9 +138,6 @@ final class NavigationBarController { @Nullable Insets mLastInsets; - @Nullable - private BroadcastReceiver mSystemOverlayChangedReceiver; - private boolean mShouldShowImeSwitcherWhenImeIsShown; @Appearance @@ -360,14 +350,6 @@ final class NavigationBarController { }); } - private boolean imeDrawsImeNavBar() { - final Resources resources = mService.getResources(); - if (resources == null) { - return false; - } - return resources.getBoolean(com.android.internal.R.bool.config_imeDrawsImeNavBar); - } - @Override public void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { final Window window = softInputWindow.getWindow(); @@ -380,27 +362,6 @@ final class NavigationBarController { if (mDestroyed) { return; } - mImeDrawsImeNavBar = imeDrawsImeNavBar(); - if (mSystemOverlayChangedReceiver == null) { - final IntentFilter intentFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); - intentFilter.addDataScheme(IntentFilter.SCHEME_PACKAGE); - intentFilter.addDataSchemeSpecificPart("android", PatternMatcher.PATTERN_LITERAL); - mSystemOverlayChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (mDestroyed) { - return; - } - mImeDrawsImeNavBar = imeDrawsImeNavBar(); - if (mImeDrawsImeNavBar) { - installNavigationBarFrameIfNecessary(); - } else { - uninstallNavigationBarFrameIfNecessary(); - } - } - }; - mService.registerReceiver(mSystemOverlayChangedReceiver, intentFilter); - } installNavigationBarFrameIfNecessary(); } @@ -413,10 +374,6 @@ final class NavigationBarController { mTintAnimator.cancel(); mTintAnimator = null; } - if (mSystemOverlayChangedReceiver != null) { - mService.unregisterReceiver(mSystemOverlayChangedReceiver); - mSystemOverlayChangedReceiver = null; - } mDestroyed = true; } @@ -454,26 +411,38 @@ final class NavigationBarController { return; } + final boolean imeDrawsImeNavBar = + (navButtonFlags & InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR) != 0; final boolean shouldShowImeSwitcherWhenImeIsShown = (navButtonFlags & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) != 0; - if (mShouldShowImeSwitcherWhenImeIsShown == shouldShowImeSwitcherWhenImeIsShown) { - return; - } + + mImeDrawsImeNavBar = imeDrawsImeNavBar; + final boolean prevShouldShowImeSwitcherWhenImeIsShown = + mShouldShowImeSwitcherWhenImeIsShown; mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; - if (mNavigationBarFrame == null) { - return; - } - final NavigationBarView navigationBarView = - mNavigationBarFrame.findViewByPredicate(NavigationBarView.class::isInstance); - if (navigationBarView == null) { - return; + if (imeDrawsImeNavBar) { + installNavigationBarFrameIfNecessary(); + if (mNavigationBarFrame == null) { + return; + } + if (mShouldShowImeSwitcherWhenImeIsShown + == prevShouldShowImeSwitcherWhenImeIsShown) { + return; + } + final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( + NavigationBarView.class::isInstance); + if (navigationBarView == null) { + return; + } + final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT + | (shouldShowImeSwitcherWhenImeIsShown + ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); + navigationBarView.setNavigationIconHints(hints); + } else { + uninstallNavigationBarFrameIfNecessary(); } - final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT - | (shouldShowImeSwitcherWhenImeIsShown - ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); - navigationBarView.setNavigationIconHints(hints); } @Override diff --git a/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java b/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java index 1b37c6cbe5ae..728a42907cc4 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java +++ b/core/java/com/android/internal/inputmethod/InputMethodNavButtonFlags.java @@ -33,11 +33,17 @@ import java.lang.annotation.Retention; */ @Retention(SOURCE) @IntDef(flag = true, value = { + InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR, InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN, }) public @interface InputMethodNavButtonFlags { /** + * When set, the IME process needs to render and handle the navigation bar buttons such as the + * back button and the IME switcher button. + */ + int IME_DRAWS_IME_NAV_BAR = 1; + /** * When set, the IME switcher icon needs to be shown on the navigation bar. */ - int SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN = 1; + int SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN = 2; } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 584193654bfa..1f9d08063714 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -336,6 +336,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private final HandwritingModeController mHwController; + @GuardedBy("ImfLock.class") + @Nullable + private OverlayableSystemBooleanResourceWrapper mImeDrawsImeNavBarRes; + static class SessionState { final ClientState client; final IInputMethodInvoker method; @@ -1724,11 +1728,43 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") + private void recreateImeDrawsImeNavBarResIfNecessary(@UserIdInt int targetUserId) { + // Currently, com.android.internal.R.bool.config_imeDrawsImeNavBar is overlaid only for the + // profile parent user. + // TODO(b/221443458): See if we can make OverlayManager be aware of profile groups. + final int profileParentUserId = mUserManagerInternal.getProfileParentId(targetUserId); + if (mImeDrawsImeNavBarRes != null + && mImeDrawsImeNavBarRes.getUserId() != profileParentUserId) { + mImeDrawsImeNavBarRes.close(); + mImeDrawsImeNavBarRes = null; + } + if (mImeDrawsImeNavBarRes == null) { + final Context userContext; + if (mContext.getUserId() == profileParentUserId) { + userContext = mContext; + } else { + userContext = mContext.createContextAsUser(UserHandle.of(profileParentUserId), + 0 /* flags */); + } + mImeDrawsImeNavBarRes = OverlayableSystemBooleanResourceWrapper.create(userContext, + com.android.internal.R.bool.config_imeDrawsImeNavBar, mHandler, resource -> { + synchronized (ImfLock.class) { + if (resource == mImeDrawsImeNavBarRes) { + sendOnNavButtonFlagsChangedLocked(); + } + } + }); + } + } + + @GuardedBy("ImfLock.class") private void switchUserOnHandlerLocked(@UserIdInt int newUserId, IInputMethodClient clientToBeReset) { if (DEBUG) Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId + " currentUserId=" + mSettings.getCurrentUserId()); + recreateImeDrawsImeNavBarResIfNecessary(newUserId); + // ContentObserver should be registered again when the user is changed mSettingsObserver.registerContentObserverLocked(newUserId); @@ -1833,6 +1869,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub }); } + recreateImeDrawsImeNavBarResIfNecessary(currentUserId); + mMyPackageMonitor.register(mContext, null, UserHandle.ALL, true); mSettingsObserver.registerContentObserverLocked(currentUserId); @@ -2941,10 +2979,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @InputMethodNavButtonFlags private int getInputMethodNavButtonFlagsLocked() { + final boolean canImeDrawsImeNavBar = + mImeDrawsImeNavBarRes != null && mImeDrawsImeNavBarRes.get(); final boolean shouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherLocked( InputMethodService.IME_ACTIVE | InputMethodService.IME_VISIBLE); - return shouldShowImeSwitcherWhenImeIsShown - ? InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0; + return (canImeDrawsImeNavBar ? InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR : 0) + | (shouldShowImeSwitcherWhenImeIsShown + ? InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN : 0); } @GuardedBy("ImfLock.class") diff --git a/services/core/java/com/android/server/inputmethod/OverlayableSystemBooleanResourceWrapper.java b/services/core/java/com/android/server/inputmethod/OverlayableSystemBooleanResourceWrapper.java new file mode 100644 index 000000000000..33e7a7621340 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/OverlayableSystemBooleanResourceWrapper.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static android.content.Intent.ACTION_OVERLAY_CHANGED; + +import android.annotation.AnyThread; +import android.annotation.BoolRes; +import android.annotation.NonNull; +import android.annotation.UserHandleAware; +import android.annotation.UserIdInt; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Handler; +import android.os.PatternMatcher; +import android.util.Slog; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * A wrapper object for any boolean resource defined in {@code "android"} package, in a way that is + * aware of per-user Runtime Resource Overlay (RRO). + */ +final class OverlayableSystemBooleanResourceWrapper implements AutoCloseable { + private static final String TAG = "OverlayableSystemBooleanResourceWrapper"; + + private static final String SYSTEM_PACKAGE_NAME = "android"; + + @UserIdInt + private final int mUserId; + @NonNull + private final AtomicBoolean mValueRef; + @NonNull + private final AtomicReference<Runnable> mCleanerRef; + + /** + * Creates {@link OverlayableSystemBooleanResourceWrapper} for the given boolean resource ID + * with a value change callback for the user associated with the {@link Context}. + * + * @param userContext The {@link Context} to be used to access the resource. This needs to be + * associated with the right user because the Runtime Resource Overlay (RRO) + * is per-user configuration. + * @param boolResId The resource ID to be queried. + * @param handler {@link Handler} to be used to dispatch {@code callback}. + * @param callback The callback to be notified when the specified value might be updated. + * The callback needs to take care of spurious wakeup. The value returned from + * {@link #get()} may look to be exactly the same as the previously read value + * e.g. when the value is changed from {@code false} to {@code true} to + * {@code false} in a very short period of time, because {@link #get()} always + * does volatile-read. + * @return New {@link OverlayableSystemBooleanResourceWrapper}. + */ + @NonNull + @UserHandleAware + static OverlayableSystemBooleanResourceWrapper create(@NonNull Context userContext, + @BoolRes int boolResId, @NonNull Handler handler, + @NonNull Consumer<OverlayableSystemBooleanResourceWrapper> callback) { + + // Note that we cannot fully trust this initial value due to the dead time between obtaining + // the value here and setting up a broadcast receiver for change callback below. + // We will refresh the value again later after setting up the change callback anyway. + final AtomicBoolean valueRef = new AtomicBoolean(evaluate(userContext, boolResId)); + + final AtomicReference<Runnable> cleanerRef = new AtomicReference<>(); + + final OverlayableSystemBooleanResourceWrapper object = + new OverlayableSystemBooleanResourceWrapper(userContext.getUserId(), valueRef, + cleanerRef); + + final IntentFilter intentFilter = new IntentFilter(ACTION_OVERLAY_CHANGED); + intentFilter.addDataScheme(IntentFilter.SCHEME_PACKAGE); + intentFilter.addDataSchemeSpecificPart(SYSTEM_PACKAGE_NAME, PatternMatcher.PATTERN_LITERAL); + + final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final boolean newValue = evaluate(userContext, boolResId); + if (newValue != valueRef.getAndSet(newValue)) { + callback.accept(object); + } + } + }; + userContext.registerReceiver(broadcastReceiver, intentFilter, + null /* broadcastPermission */, handler, + Context.RECEIVER_NOT_EXPORTED); + cleanerRef.set(() -> userContext.unregisterReceiver(broadcastReceiver)); + + // Make sure that the initial observable value is obtained after the change callback is set. + valueRef.set(evaluate(userContext, boolResId)); + return object; + } + + private OverlayableSystemBooleanResourceWrapper(@UserIdInt int userId, + @NonNull AtomicBoolean valueRef, @NonNull AtomicReference<Runnable> cleanerRef) { + mUserId = userId; + mValueRef = valueRef; + mCleanerRef = cleanerRef; + } + + /** + * @return The boolean resource value. + */ + @AnyThread + boolean get() { + return mValueRef.get(); + } + + /** + * @return The user ID associated with this resource reader. + */ + @AnyThread + @UserIdInt + int getUserId() { + return mUserId; + } + + @AnyThread + private static boolean evaluate(@NonNull Context context, @BoolRes int boolResId) { + try { + return context.getPackageManager() + .getResourcesForApplication(SYSTEM_PACKAGE_NAME) + .getBoolean(boolResId); + } catch (PackageManager.NameNotFoundException e) { + Slog.e(TAG, "getResourcesForApplication(\"" + SYSTEM_PACKAGE_NAME + "\") failed", e); + return false; + } + } + + /** + * Cleans up the callback. + */ + @AnyThread + @Override + public void close() { + final Runnable cleaner = mCleanerRef.getAndSet(null); + if (cleaner != null) { + cleaner.run(); + } + } +} |