/* * 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.systemui.car.input; import static android.car.CarOccupantZoneManager.DISPLAY_TYPE_MAIN; import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER; import android.annotation.NonNull; import android.annotation.Nullable; import android.car.Car; import android.car.CarOccupantZoneManager; import android.car.CarOccupantZoneManager.OccupantZoneInfo; import android.car.hardware.power.CarPowerManager; import android.car.settings.CarSettings; import android.content.Context; import android.database.ContentObserver; import android.hardware.display.DisplayManager; import android.net.Uri; import android.os.Handler; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.ArraySet; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.view.MotionEvent; import android.widget.Toast; import androidx.annotation.MainThread; import androidx.annotation.VisibleForTesting; import com.android.systemui.CoreStartable; import com.android.systemui.R; import com.android.systemui.car.CarServiceProvider; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import java.io.PrintWriter; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; import javax.inject.Inject; /** * Controls {@link DisplayInputSink}. It can be used for the display input lock or display input * monitor. * */ @SysUISingleton public final class DisplayInputSinkController implements CoreStartable { private static final String TAG = "DisplayInputLock"; // 4 displays would be enough for most systems. private static final int INITIAL_INPUT_SINK_CAPACITY = 4; static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static final Uri DISPLAY_INPUT_LOCK_URI = Settings.Global.getUriFor(CarSettings.Global.DISPLAY_INPUT_LOCK); private final Context mContext; private final CarServiceProvider mCarServiceProvider; private final Handler mHandler; private final DisplayManager mDisplayManager; private final ContentObserver mSettingsObserver; // Map of input sinks per display that are currently on going. (key: displayId) private final SparseArray mDisplayInputSinks; // Map of input locks that are currently on going. (key: displayId) private final ArraySet mDisplayInputLockedDisplays; // A set of display unique ids from the display input lock setting. private final ArraySet mDisplayInputLockSetting; // Map of the available passenger displays. (key: displayId) private final SparseArray mPassengerDisplays; private CarOccupantZoneManager mOccupantZoneManager; private CarPowerManager mCarPowerManager; @VisibleForTesting final DisplayManager.DisplayListener mDisplayListener = new DisplayManager.DisplayListener() { @Override @MainThread public void onDisplayAdded(int displayId) { mayUpdatePassengerDisplayOnAdded(displayId); refreshDisplayInputSink(displayId, "onDisplayAdded"); } @Override @MainThread public void onDisplayRemoved(int displayId) { if (!mPassengerDisplays.contains(displayId)) return; mayStopDisplayInputLock(mDisplayManager.getDisplay(displayId)); mayStopDisplayInputMonitor(displayId); mPassengerDisplays.remove(displayId); } @Override @MainThread public void onDisplayChanged(int displayId) { refreshDisplayInputSink(displayId, "onDisplayChanged"); } }; private void refreshDisplayInputSink(int displayId, String caller) { int index = mPassengerDisplays.indexOfKey(displayId); if (index < 0) { if (DBG) Slog.d(TAG, caller + ": Not a passenger display#" + displayId); return; } decideDisplayInputSink(index); } @Inject public DisplayInputSinkController(Context context, @Main Handler handler, CarServiceProvider carServiceProvider) { this(context, handler, carServiceProvider, new SparseArray(INITIAL_INPUT_SINK_CAPACITY), new ArraySet(INITIAL_INPUT_SINK_CAPACITY), new ArraySet(INITIAL_INPUT_SINK_CAPACITY), new SparseArray(INITIAL_INPUT_SINK_CAPACITY)); } @VisibleForTesting DisplayInputSinkController(Context context, @Main Handler handler, CarServiceProvider carServiceProvider, SparseArray displayInputSinks, ArraySet displayInputLockedDisplays, ArraySet displayInputLockSetting, SparseArray passengerDisplays) { mContext = context; mHandler = handler; mDisplayManager = mContext.getSystemService(DisplayManager.class); mSettingsObserver = new ContentObserver(mHandler) { @Override @MainThread public void onChange(boolean selfChange, Uri uri) { if (DBG) Slog.d(TAG, "onChange: self=" + selfChange + ", uri=" + uri); refreshDisplayInputLockSetting(); } }; mCarServiceProvider = carServiceProvider; mDisplayInputSinks = displayInputSinks; mDisplayInputLockedDisplays = displayInputLockedDisplays; mDisplayInputLockSetting = displayInputLockSetting; mPassengerDisplays = passengerDisplays; } @Override public void start() { if (UserHandle.myUserId() != UserHandle.USER_SYSTEM && UserManager.isHeadlessSystemUserMode()) { Slog.i(TAG, "Disable DisplayInputSinkController for non system user " + UserHandle.myUserId()); return; } mCarServiceProvider.addListener(mCarServiceOnConnectedListener); mContext.getContentResolver().registerContentObserver(DISPLAY_INPUT_LOCK_URI, /* notifyForDescendants= */ false, mSettingsObserver); mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); } private final CarServiceProvider.CarServiceOnConnectedListener mCarServiceOnConnectedListener = new CarServiceProvider.CarServiceOnConnectedListener() { @Override public void onConnected(Car car) { mOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class); mCarPowerManager = car.getCarManager(CarPowerManager.class); initPassengerDisplays(); refreshDisplayInputLockSetting(); } }; // Assumes that all main displays for passengers are static. private void initPassengerDisplays() { List allZones = mOccupantZoneManager.getAllOccupantZones(); for (int i = allZones.size() - 1; i >= 0; --i) { OccupantZoneInfo zone = allZones.get(i); if (zone.occupantType == OCCUPANT_TYPE_DRIVER) continue; // Skip a driver. Display display = mOccupantZoneManager.getDisplayForOccupant(zone, DISPLAY_TYPE_MAIN); if (display == null) { Slog.w(TAG, "Can't access the display of zone=" + zone); continue; } mPassengerDisplays.put(display.getDisplayId(), display); } } private void mayUpdatePassengerDisplayOnAdded(int displayId) { if (mPassengerDisplays.contains(displayId)) { // Display is already added to the passenger display list. return; } if (mOccupantZoneManager == null) { Slog.w(TAG, "CarService isn't connected yet"); return; } OccupantZoneInfo zone = mOccupantZoneManager.getOccupantZoneForDisplayId(displayId); if (zone == null) { Slog.w(TAG, "Can't find the zone info for display#" + displayId); return; } if (zone.occupantType == OCCUPANT_TYPE_DRIVER) { // Skip a driver display return; } Display display = mOccupantZoneManager.getDisplayForOccupant(zone, DISPLAY_TYPE_MAIN); if (display == null) { Slog.w(TAG, "Can't access the display of zone=" + zone); return; } mPassengerDisplays.put(displayId, display); } // Start/stop display input locks from the current global setting. @VisibleForTesting void refreshDisplayInputLockSetting() { String settingValue = getDisplayInputLockSettingValue(); parseDisplayInputLockSettingValue(CarSettings.Global.DISPLAY_INPUT_LOCK, settingValue); if (DBG) { Slog.d(TAG, "refreshDisplayInputLock: settingValue=" + settingValue); } for (int i = mPassengerDisplays.size() - 1; i >= 0; --i) { decideDisplayInputSink(i); } } private void decideDisplayInputSink(int index) { int displayId = mPassengerDisplays.keyAt(index); Display display = mPassengerDisplays.valueAt(index); if (mDisplayInputLockSetting.contains(display.getUniqueId())) { mayStopDisplayInputMonitor(displayId); mayStartDisplayInputLock(display); } else if (Display.isOffState(display.getState())) { mayStopDisplayInputLock(display); mayStartDisplayInputMonitor(display); } else { mayStopDisplayInputLock(display); mayStopDisplayInputMonitor(displayId); } } private String getDisplayInputLockSettingValue() { return Settings.Global.getString(mContext.getContentResolver(), CarSettings.Global.DISPLAY_INPUT_LOCK); } private void parseDisplayInputLockSettingValue(@NonNull String settingKey, @Nullable String value) { mDisplayInputLockSetting.clear(); if (value == null || value.isEmpty()) { return; } String[] entries = value.split(","); int numEntries = entries.length; mDisplayInputLockSetting.ensureCapacity(numEntries); for (int i = 0; i < numEntries; i++) { String uniqueId = entries[i]; if (findDisplayIdByUniqueId(uniqueId) == Display.INVALID_DISPLAY) { Slog.w(TAG, "Invalid display id: " + uniqueId); continue; } mDisplayInputLockSetting.add(uniqueId); } } private int findDisplayIdByUniqueId(@NonNull String displayUniqueId) { for (int i = mPassengerDisplays.size() - 1; i >= 0; --i) { Display display = mPassengerDisplays.valueAt(i); if (displayUniqueId.equals(display.getUniqueId())) { return display.getDisplayId(); } } return Display.INVALID_DISPLAY; } private boolean isDisplayInputLockStarted(int displayId) { return mDisplayInputLockedDisplays.contains(displayId); } private boolean isDisplayInputMonitorStarted(int displayId) { return !isDisplayInputLockStarted(displayId) && mDisplayInputSinks.get(displayId) != null; } @VisibleForTesting void mayStartDisplayInputLock(@NonNull Display display) { int displayId = display.getDisplayId(); if (isDisplayInputLockStarted(displayId)) { // Already started input lock for the given display. if (DBG) Slog.d(TAG, "Input lock is already started for display#" + displayId); return; } Slog.i(TAG, "Start input lock for display " + displayId); mDisplayInputLockedDisplays.add(displayId); Context displayContext = mContext.createDisplayContext(display); AtomicReference toastRef = new AtomicReference<>(null); UnaryOperator cancelToast = (toast) -> { toast.cancel(); return toast; }; UnaryOperator createToast = (toast) -> Toast.makeText(displayContext, R.string.display_input_lock_text, Toast.LENGTH_SHORT); DisplayInputSink.OnInputEventListener callback = (event) -> { if (DBG) { Slog.d(TAG, "Received input events while input is locked for display#" + event.getDisplayId()); } if (mCarPowerManager != null) { mCarPowerManager.notifyUserActivity(event.getDisplayId()); } Runnable r = () -> { // MotionEvents for clicks are ACTION_DOWN + ACTION_UP // Only capture one of those events so the Toast shows once per click if (event instanceof MotionEvent && ((MotionEvent) event).getAction() == MotionEvent.ACTION_DOWN) { if (toastRef.get() != null) { toastRef.updateAndGet(cancelToast); } toastRef.updateAndGet(createToast).show(); } }; mHandler.post(r); }; mDisplayInputSinks.put(displayId, new DisplayInputSink(display, callback)); // Now that the display input lock is started, let's inform the user of it. mHandler.post(() -> Toast.makeText(displayContext, R.string.display_input_lock_started_text, Toast.LENGTH_SHORT).show()); } private void mayStartDisplayInputMonitor(Display display) { int displayId = display.getDisplayId(); if (isDisplayInputMonitorStarted(displayId)) { // Already started input monitor for the given display. if (DBG) Slog.d(TAG, "Input monitor is already started for display#" + displayId); return; } Slog.i(TAG, "Start input monitor for display#" + displayId); DisplayInputSink.OnInputEventListener callback = (event) -> { if (DBG) { Slog.d(TAG, "Received input events for monitored display#" + event.getDisplayId()); } if (mCarPowerManager != null) { mCarPowerManager.notifyUserActivity(event.getDisplayId()); } }; mDisplayInputSinks.put(displayId, new DisplayInputSink(display, callback)); } @VisibleForTesting void mayStopDisplayInputLock(Display display) { int displayId = display.getDisplayId(); if (!isDisplayInputLockStarted(displayId)) { if (DBG) Slog.d(TAG, "There is no input lock started for display#" + displayId); return; } Slog.i(TAG, "Stop input lock for display#" + displayId); mHandler.post(() -> Toast.makeText(mContext.createDisplayContext(display), R.string.display_input_lock_stopped_text, Toast.LENGTH_SHORT).show()); removeDisplayInputSink(displayId); mDisplayInputLockedDisplays.remove(displayId); } private void mayStopDisplayInputMonitor(int displayId) { if (!isDisplayInputMonitorStarted(displayId)) { if (DBG) Slog.d(TAG, "There is no input monitor started for display#" + displayId); return; } Slog.i(TAG, "Stop input monitor for display#" + displayId); removeDisplayInputSink(displayId); } private void removeDisplayInputSink(int displayId) { int index = mDisplayInputSinks.indexOfKey(displayId); if (index < 0) { throw new IllegalStateException("Can't find the input sink for display#" + displayId); } DisplayInputSink inputLock = mDisplayInputSinks.valueAt(index); inputLock.release(); mDisplayInputSinks.removeAt(index); } @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("DisplayInputSinks:"); int size = mDisplayInputSinks.size(); for (int i = 0; i < size; i++) { DisplayInputSink inputSink = mDisplayInputSinks.valueAt(i); pw.printf(" %d: %s\n", i, inputSink.toString()); } pw.println("DisplayInputLockedWindows:"); size = mDisplayInputLockedDisplays.size(); for (int i = 0; i < size; i++) { pw.printf(" %s\n", mDisplayInputLockedDisplays.valueAt(i).toString()); } pw.printf("DisplayInputLockSetting: %s\n", mDisplayInputLockSetting); pw.print("PassegnerDisplays: ["); for (int i = mPassengerDisplays.size() - 1; i >= 0; --i) { pw.print(mPassengerDisplays.keyAt(i)); if (i > 0) pw.print(", "); } pw.println(']'); } }