Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7570cac6 authored by lumark's avatar lumark
Browse files

Propogate per-display IME status to SysUI

CL [1] introduced SystemUI to support updating IME window status per
display. (i.e. IStatusBarService#setImeWindowStatus left TODO item to
add displayId parameter to select the target display.)

For IMMS, we applied this API base on current token's displayId, and refined
the code flow in Sysui to reset IME window state for non-target display's
NavBar when single IME switch to another display case.

And make sure some cases in IMMS should reset IME window status:
- When current method unbind.
- When current top window focus display is not same as current token's display.
  (That means the input session may connected but IME window not
  yet attached. Note that it doesn't include external display without
  system decoration and show IME window on default display case since it
  is intentional behavior and we still need to update status for this
  case.)

Also added testSetImeWindowStatusWhenImeSwitchOnDisplay in
NavigationBarFragmentTest to enhance the ability of verifying external
navigation bar.

[1]: 24e7a9fd

Bug: 127309955
Bug: 117478341
Test: atest NavigationBarFragmentTest
Test: manual as below steps:
      - Pre-condition:
          1. Enable desktop mode.
          2. Create simulated display.
      - Use case 1):
          1. Launch an activity with input field on simulated display.
          2. Bring up the IME there, expect back key icon on external
             NavBar will changed when IME bring up.
          3. Tap on primary display or launch an activity there.
	     (or pressing home key)
          4. Expect IME will hide & back key icon on external NavBar will
             set back.
      - Use case 2):
          1. Launch activity with input field on default display.
          2. Tapping EditText in activity on default display.
	  3. Expect IME will bring up here and back key icon on default
	     display will changed when IME bring up.
          4. Launch activity without input field in external display.
	     (i.e. clock app)
          5. Expect Both Default & external display's nav bar back key icon
	     will set back.

Change-Id: Ia414b8aea631e295cccd6f6da44d04bad16545c7
parent 3ba6ace8
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -48,8 +48,7 @@ interface IStatusBarService
    void setIconVisibility(String slot, boolean visible);
    @UnsupportedAppUsage
    void removeIcon(String slot);
    // TODO(b/117478341): support back button change when IME is showing on a external display.
    void setImeWindowStatus(in IBinder token, int vis, int backDisposition,
    void setImeWindowStatus(int displayId, in IBinder token, int vis, int backDisposition,
            boolean showImeSwitcher);
    void expandSettingsPanel(String subPanel);

+38 −4
Original line number Diff line number Diff line
@@ -18,7 +18,10 @@ package com.android.systemui.statusbar;

import static android.app.StatusBarManager.DISABLE2_NONE;
import static android.app.StatusBarManager.DISABLE_NONE;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
import static android.inputmethodservice.InputMethodService.IME_INVISIBLE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;

import static com.android.systemui.statusbar.phone.StatusBar.ONLY_CORE_APPS;

@@ -40,6 +43,7 @@ import android.os.Looper;
import android.os.Message;
import android.util.Pair;
import android.util.SparseArray;
import android.view.inputmethod.InputMethodSystemProperty;

import androidx.annotation.VisibleForTesting;

@@ -127,6 +131,11 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController<
    private Handler mHandler = new H(Looper.getMainLooper());
    /** A map of display id - disable flag pair */
    private SparseArray<Pair<Integer, Integer>> mDisplayDisabled = new SparseArray<>();
    /**
     * The last ID of the display where IME window for which we received setImeWindowStatus
     * event.
     */
    private int mLastUpdatedImeDisplayId = INVALID_DISPLAY;

    /**
     * These methods are called back on the main thread.
@@ -785,6 +794,32 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController<
        }
    }

    private void handleShowImeButton(int displayId, IBinder token, int vis, int backDisposition,
            boolean showImeSwitcher) {
        if (displayId == INVALID_DISPLAY) return;

        if (!InputMethodSystemProperty.MULTI_CLIENT_IME_ENABLED
                && mLastUpdatedImeDisplayId != displayId
                && mLastUpdatedImeDisplayId != INVALID_DISPLAY) {
            // Set previous NavBar's IME window status as invisible when IME
            // window switched to another display for single-session IME case.
            sendImeInvisibleStatusForPrevNavBar();
        }
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).setImeWindowStatus(displayId, token, vis, backDisposition,
                    showImeSwitcher);
        }
        mLastUpdatedImeDisplayId = displayId;
    }

    private void sendImeInvisibleStatusForPrevNavBar() {
        for (int i = 0; i < mCallbacks.size(); i++) {
            mCallbacks.get(i).setImeWindowStatus(mLastUpdatedImeDisplayId,
                    null /* token */, IME_INVISIBLE, BACK_DISPOSITION_DEFAULT,
                    false /* showImeSwitcher */);
        }
    }

    private final class H extends Handler {
        private H(Looper l) {
            super(l);
@@ -852,10 +887,9 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController<
                    break;
                case MSG_SHOW_IME_BUTTON:
                    args = (SomeArgs) msg.obj;
                    for (int i = 0; i < mCallbacks.size(); i++) {
                        mCallbacks.get(i).setImeWindowStatus(args.argi1, (IBinder) args.arg1,
                                args.argi2, args.argi3, args.argi4 != 0 /* showImeSwitcher */);
                    }
                    handleShowImeButton(args.argi1 /* displayId */, (IBinder) args.arg1 /* token */,
                            args.argi2 /* vis */, args.argi3 /* backDisposition */,
                            args.argi4 != 0 /* showImeSwitcher */);
                    break;
                case MSG_SHOW_RECENT_APPS:
                    for (int i = 0; i < mCallbacks.size(); i++) {
+5 −0
Original line number Diff line number Diff line
@@ -1064,4 +1064,9 @@ public class NavigationBarFragment extends LifecycleFragment implements Callback
        context.getSystemService(WindowManager.class).addView(navigationBarView, lp);
        return navigationBarView;
    }

    @VisibleForTesting
    int getNavigationIconHints() {
        return mNavigationIconHints;
    }
}
+165 −5
Original line number Diff line number Diff line
@@ -14,18 +14,38 @@

package com.android.systemui.statusbar.phone;

import static android.app.StatusBarManager.NAVIGATION_HINT_BACK_ALT;
import static android.app.StatusBarManager.NAVIGATION_HINT_IME_SHOWN;
import static android.inputmethodservice.InputMethodService.BACK_DISPOSITION_DEFAULT;
import static android.inputmethodservice.InputMethodService.IME_VISIBLE;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.annotation.LayoutRes;
import android.annotation.Nullable;
import android.app.Fragment;
import android.app.FragmentController;
import android.app.FragmentHostCallback;
import android.content.Context;
import android.hardware.display.DisplayManagerGlobal;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import android.testing.LeakCheck.Tracker;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityManager.AccessibilityServicesStateChangeListener;

@@ -34,6 +54,7 @@ import androidx.test.filters.SmallTest;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Dependency;
import com.android.systemui.SysuiBaseFragmentTest;
import com.android.systemui.SysuiTestableContext;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.recents.Recents;
@@ -50,9 +71,16 @@ import org.junit.runner.RunWith;
@RunWithLooper()
@SmallTest
public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
    private static final int EXTERNAL_DISPLAY_ID = 2;
    private static final int NAV_BAR_VIEW_ID = 43;

    private Fragment mFragmentExternalDisplay;
    private FragmentController mControllerExternalDisplay;

    private SysuiTestableContext mSysuiTestableContextExternal;
    private OverviewProxyService mOverviewProxyService =
            mDependency.injectMockDependency(OverviewProxyService.class);
    private CommandQueue mCommandQueue;
    private AccessibilityManagerWrapper mAccessibilityWrapper =
            new AccessibilityManagerWrapper(mContext) {
                Tracker mTracker = mLeakCheck.getTracker("accessibility_manager");
@@ -73,15 +101,51 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
    }

    protected void createRootView() {
        mView = new NavigationBarFrame(mContext);
        mView = new NavigationBarFrame(mSysuiContext);
        mView.setId(NAV_BAR_VIEW_ID);
    }

    @Before
    public void setup() {
        mSysuiContext.putComponent(CommandQueue.class, mock(CommandQueue.class));
    public void setupFragment() throws Exception {
        setupSysuiDependency();
        createRootView();
        TestableLooper.get(this).runWithLooper(() -> {
            mHandler = new Handler();

            mFragment = instantiate(mSysuiContext, NavigationBarFragment.class.getName(), null);
            mFragments = FragmentController.createController(
                    new HostCallbacksForExternalDisplay(mSysuiContext));
            mFragments.attachHost(null);
            mFragments.getFragmentManager().beginTransaction()
                    .replace(NAV_BAR_VIEW_ID, mFragment)
                    .commit();
            mControllerExternalDisplay = FragmentController.createController(
                    new HostCallbacksForExternalDisplay(mSysuiTestableContextExternal));
            mControllerExternalDisplay.attachHost(null);
            mFragmentExternalDisplay = instantiate(mSysuiTestableContextExternal,
                    NavigationBarFragment.class.getName(), null);
            mControllerExternalDisplay.getFragmentManager().beginTransaction()
                    .replace(NAV_BAR_VIEW_ID, mFragmentExternalDisplay)
                    .commit();
        });
    }

    private void setupSysuiDependency() {
        mCommandQueue = new CommandQueue(mContext);
        mSysuiContext.putComponent(CommandQueue.class, mCommandQueue);
        mSysuiContext.putComponent(StatusBar.class, mock(StatusBar.class));
        mSysuiContext.putComponent(Recents.class, mock(Recents.class));
        mSysuiContext.putComponent(Divider.class, mock(Divider.class));

        Display display = new Display(DisplayManagerGlobal.getInstance(), EXTERNAL_DISPLAY_ID,
                new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
        mSysuiTestableContextExternal = (SysuiTestableContext) mSysuiContext.createDisplayContext(
                display);
        mSysuiTestableContextExternal.putComponent(CommandQueue.class, mCommandQueue);
        mSysuiTestableContextExternal.putComponent(StatusBar.class, mock(StatusBar.class));
        mSysuiTestableContextExternal.putComponent(Recents.class, mock(Recents.class));
        mSysuiTestableContextExternal.putComponent(Divider.class, mock(Divider.class));

        injectLeakCheckedDependencies(ALL_SUPPORTED_CLASSES);
        WindowManager windowManager = mock(WindowManager.class);
        Display defaultDisplay = mContext.getSystemService(WindowManager.class).getDefaultDisplay();
@@ -102,15 +166,111 @@ public class NavigationBarFragmentTest extends SysuiBaseFragmentTest {
        navigationBarFragment.onHomeLongClick(navigationBarFragment.getView());
    }

    @Test
    public void testSetImeWindowStatusWhenImeSwitchOnDisplay() {
        // Create default & external NavBar fragment.
        NavigationBarFragment defaultNavBar = (NavigationBarFragment) mFragment;
        NavigationBarFragment externalNavBar = (NavigationBarFragment) mFragmentExternalDisplay;
        mFragments.dispatchCreate();
        processAllMessages();
        mFragments.dispatchResume();
        processAllMessages();
        mControllerExternalDisplay.dispatchCreate();
        processAllMessages();
        mControllerExternalDisplay.dispatchResume();
        processAllMessages();

        // Set IME window status for default NavBar.
        mCommandQueue.setImeWindowStatus(DEFAULT_DISPLAY, null, IME_VISIBLE,
                BACK_DISPOSITION_DEFAULT, true);
        Handler.getMain().runWithScissors(() -> { }, 500);

        // Verify IME window state will be updated in default NavBar & external NavBar state reset.
        assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
                defaultNavBar.getNavigationIconHints());
        assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
        assertFalse((externalNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);

        // Set IME window status for external NavBar.
        mCommandQueue.setImeWindowStatus(EXTERNAL_DISPLAY_ID, null,
                IME_VISIBLE, BACK_DISPOSITION_DEFAULT, true);
        Handler.getMain().runWithScissors(() -> { }, 500);

        // Verify IME window state will be updated in external NavBar & default NavBar state reset.
        assertEquals(NAVIGATION_HINT_BACK_ALT | NAVIGATION_HINT_IME_SHOWN,
                externalNavBar.getNavigationIconHints());
        assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_BACK_ALT) != 0);
        assertFalse((defaultNavBar.getNavigationIconHints() & NAVIGATION_HINT_IME_SHOWN) != 0);
    }

    @Override
    protected Fragment instantiate(Context context, String className, Bundle arguments) {
        DeviceProvisionedController deviceProvisionedController =
                mock(DeviceProvisionedController.class);
        assertNotNull(mAccessibilityWrapper);
        return new NavigationBarFragment(mAccessibilityWrapper,
        return new NavigationBarFragment(
                context.getDisplayId() == DEFAULT_DISPLAY ? mAccessibilityWrapper
                        : mock(AccessibilityManagerWrapper.class),
                deviceProvisionedController,
                new MetricsLogger(),
                new AssistManager(deviceProvisionedController, mContext),
                mock(AssistManager.class),
                mOverviewProxyService);
    }

    private class HostCallbacksForExternalDisplay extends
            FragmentHostCallback<NavigationBarFragmentTest> {
        private Context mDisplayContext;

        HostCallbacksForExternalDisplay(Context context) {
            super(context, mHandler, 0);
            mDisplayContext = context;
        }

        @Override
        public NavigationBarFragmentTest onGetHost() {
            return NavigationBarFragmentTest.this;
        }

        @Override
        public Fragment instantiate(Context context, String className, Bundle arguments) {
            return NavigationBarFragmentTest.this.instantiate(context, className, arguments);
        }

        @Override
        public View onFindViewById(int id) {
            return mView.findViewById(id);
        }

        @Override
        public LayoutInflater onGetLayoutInflater() {
            return new LayoutInflaterWrapper(mDisplayContext);
        }
    }

    private static class LayoutInflaterWrapper extends LayoutInflater {
        protected LayoutInflaterWrapper(Context context) {
            super(context);
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return null;
        }

        @Override
        public View inflate(@LayoutRes int resource, @Nullable ViewGroup root,
                boolean attachToRoot) {
            NavigationBarView view = mock(NavigationBarView.class);
            when(view.getDisplay()).thenReturn(mContext.getDisplay());
            when(view.getBackButton()).thenReturn(mock(ButtonDispatcher.class));
            when(view.getHomeButton()).thenReturn(mock(ButtonDispatcher.class));
            when(view.getRecentsButton()).thenReturn(mock(ButtonDispatcher.class));
            when(view.getAccessibilityButton()).thenReturn(mock(ButtonDispatcher.class));
            when(view.getRotateSuggestionButton()).thenReturn(mock(RotationContextButton.class));
            when(view.getBarTransitions()).thenReturn(mock(BarTransitions.class));
            when(view.getLightTransitionsController()).thenReturn(
                    mock(LightBarTransitionsController.class));
            return view;
        }
    }
}
+28 −8
Original line number Diff line number Diff line
@@ -575,6 +575,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
     */
    int mCurTokenDisplayId = INVALID_DISPLAY;

    /**
     * The display ID of the input method indicates the fallback display which returned by
     * {@link #computeImeDisplayIdForTarget}.
     */
    private static final int FALLBACK_DISPLAY_ID = DEFAULT_DISPLAY;

    final ImeDisplayValidator mImeDisplayValidator;

    /**
@@ -625,7 +631,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
     *    currently invisible.
     * </dd>
     * </dl>
     * <em>Do not update this value outside of setImeWindowStatus.</em>
     * <em>Do not update this value outside of {@link #setImeWindowStatus(IBinder, int, int)} and
     * {@link #unbindCurrentMethodLocked()}.</em>
     */
    int mImeWindowVis;

@@ -2124,12 +2131,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
     */
    static int computeImeDisplayIdForTarget(int displayId, @NonNull ImeDisplayValidator checker) {
        if (displayId == DEFAULT_DISPLAY || displayId == INVALID_DISPLAY) {
            // We always assume that the default display id suitable to show the IME window.
            return DEFAULT_DISPLAY;
            return FALLBACK_DISPLAY_ID;
        }
        // Show IME in default display when the display with IME target doesn't support system
        // decorations.
        return checker.displayCanShowIme(displayId) ? displayId : DEFAULT_DISPLAY;
        // Show IME window on fallback display when the display is not allowed.
        return checker.displayCanShowIme(displayId) ? displayId : FALLBACK_DISPLAY_ID;
    }

    @Override
@@ -2198,6 +2203,10 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
                mIWindowManager.removeWindowToken(mCurToken, mCurTokenDisplayId);
            } catch (RemoteException e) {
            }
            // Set IME window status as invisible when unbind current method.
            mImeWindowVis = 0;
            mBackDisposition = InputMethodService.BACK_DISPOSITION_DEFAULT;
            updateSystemUiLocked(mImeWindowVis, mBackDisposition);
            mCurToken = null;
            mCurTokenDisplayId = INVALID_DISPLAY;
        }
@@ -2399,10 +2408,20 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
    @BinderThread
    @SuppressWarnings("deprecation")
    private void setImeWindowStatus(@NonNull IBinder token, int vis, int backDisposition) {
        final int topFocusedDisplayId = mWindowManagerInternal.getTopFocusedDisplayId();

        synchronized (mMethodMap) {
            if (!calledWithValidTokenLocked(token)) {
                return;
            }
            // Skip update IME status when current token display is not same as focused display.
            // Note that we still need to update IME status when focusing external display
            // that does not support system decoration and fallback to show IME on default
            // display since it is intentional behavior.
            if (mCurTokenDisplayId != topFocusedDisplayId
                    && mCurTokenDisplayId != FALLBACK_DISPLAY_ID) {
                return;
            }
            mImeWindowVis = vis;
            mBackDisposition = backDisposition;
            updateSystemUiLocked(vis, backDisposition);
@@ -2447,7 +2466,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
        if (DEBUG) {
            Slog.d(TAG, "IME window vis: " + vis
                    + " active: " + (vis & InputMethodService.IME_ACTIVE)
                    + " inv: " + (vis & InputMethodService.IME_INVISIBLE));
                    + " inv: " + (vis & InputMethodService.IME_INVISIBLE)
                    + " displayId: " + mCurTokenDisplayId);
        }

        // TODO: Move this clearing calling identity block to setImeWindowStatus after making sure
@@ -2461,7 +2481,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
            // mImeWindowVis should be updated before calling shouldShowImeSwitcherLocked().
            final boolean needsToShowImeSwitcher = shouldShowImeSwitcherLocked(vis);
            if (mStatusBar != null) {
                mStatusBar.setImeWindowStatus(mCurToken, vis, backDisposition,
                mStatusBar.setImeWindowStatus(mCurTokenDisplayId, mCurToken, vis, backDisposition,
                        needsToShowImeSwitcher);
            }
            final InputMethodInfo imi = mMethodMap.get(mCurMethodId);
Loading