/* * Copyright (C) 2023 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.systemui.wm; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.RemoteException; import android.util.Log; import android.view.Display; import android.view.IDisplayWindowInsetsController; import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.WindowInsets; import android.view.inputmethod.ImeTracker; import androidx.annotation.BinderThread; import androidx.annotation.MainThread; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.car.systembar.CarSystemBar; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.statusbar.CommandQueue; import java.util.HashSet; import java.util.Set; /** * b/259604616, This controller is created as a workaround for NavBar issues in concurrent * {@link CarSystemBar}/SystemUI. * Problem: CarSystemBar relies on {@link IStatusBarService}, * which can register only one process to listen for the {@link CommandQueue} events. * Solution: {@link MDSystemBarsController} intercepts Insets change event by registering the * {@link BinderThread} with * {@link IWindowManager#setDisplayWindowInsetsController(int, IDisplayWindowInsetsController)} and * notifies its listener for both Primary and Secondary SystemUI * process. */ public class MDSystemBarsController { private static final String TAG = MDSystemBarsController.class.getSimpleName(); private static final boolean DEBUG = Build.IS_ENG || Build.IS_USERDEBUG; private Set mListeners; private int mDisplayId = Display.INVALID_DISPLAY; private InsetsState mCurrentInsetsState; private final IWindowManager mIWindowManager; private final Handler mMainHandler; private final Context mContext; public MDSystemBarsController( IWindowManager wmService, @Main Handler mainHandler, Context context) { mIWindowManager = wmService; mMainHandler = mainHandler; mContext = context; } /** * Adds a listener for the display. * Adding a listener to a Display, replaces previous binder callback to this * displayId * {@link IWindowManager#setDisplayWindowInsetsController(int, IDisplayWindowInsetsController)} * A SystemUI process should only register to a single display with displayId * {@link Context#getDisplayId()} * * Note: {@link Context#getDisplayId()} will return the {@link Context#DEVICE_ID_DEFAULT}, if * called in the constructor. As this component's constructor is called before the DisplayId * gets assigned to the context. * * @param listener SystemBar Inset events */ @MainThread public void addListener(Listener listener) { if (mDisplayId != Display.INVALID_DISPLAY && mDisplayId != mContext.getDisplayId()) { Log.e(TAG, "Unexpected Display Id change"); mListeners = null; mCurrentInsetsState = null; unregisterWindowInsetController(mDisplayId); } if (mListeners != null) { mListeners.add(listener); return; } mDisplayId = mContext.getDisplayId(); mListeners = new HashSet<>(); mListeners.add(listener); registerWindowInsetController(mDisplayId); } private void registerWindowInsetController(int displayId) { if (DEBUG) { Log.d(TAG, "Registering a WindowInsetController with Display: " + displayId); } try { mIWindowManager.setDisplayWindowInsetsController(displayId, new DisplayWindowInsetsControllerImpl()); } catch (RemoteException e) { Log.w(TAG, "Unable to set insets controller on display " + displayId); } } private void unregisterWindowInsetController(int displayId) { if (DEBUG) { Log.d(TAG, "Unregistering a WindowInsetController with Display: " + displayId); } try { mIWindowManager.setDisplayWindowInsetsController(displayId, null); } catch (RemoteException e) { Log.w(TAG, "Unable to remove insets controller on display " + displayId); } } @BinderThread private class DisplayWindowInsetsControllerImpl extends IDisplayWindowInsetsController.Stub { @Override public void topFocusedWindowChanged(ComponentName component, @WindowInsets.Type.InsetsType int requestedVisibleTypes) { //no-op } @Override public void insetsChanged(InsetsState insetsState) { if (insetsState == null || insetsState.equals(mCurrentInsetsState)) { return; } mCurrentInsetsState = insetsState; if (mListeners == null) { return; } boolean show = insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime()); mMainHandler.post(() -> { for (Listener l : mListeners) { l.onKeyboardVisibilityChanged(show); } }); } @Override public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) { //no-op } @Override public void showInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) { //no-op } @Override public void hideInsets(@WindowInsets.Type.InsetsType int types, boolean fromIme, @Nullable ImeTracker.Token statsToken) { //no-op } @Override public void setImeInputTargetRequestedVisibility(boolean visible) { //no-op } } /** * Remove a listener for a display * * @param listener SystemBar Inset events Listener * @return if set contains such a listener, returns {@code true} otherwise false */ public boolean removeListener(Listener listener) { if (mListeners == null) { return false; } return mListeners.remove(listener); } /** * Listener for SystemBar insets events */ public interface Listener { /** * show/hide keyboard */ void onKeyboardVisibilityChanged(boolean showing); } }