1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.car.input; 18 19 import static android.car.CarOccupantZoneManager.DISPLAY_TYPE_MAIN; 20 import static android.car.CarOccupantZoneManager.OCCUPANT_TYPE_DRIVER; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.car.Car; 25 import android.car.CarOccupantZoneManager; 26 import android.car.CarOccupantZoneManager.OccupantZoneInfo; 27 import android.car.hardware.power.CarPowerManager; 28 import android.car.settings.CarSettings; 29 import android.content.Context; 30 import android.database.ContentObserver; 31 import android.hardware.display.DisplayManager; 32 import android.net.Uri; 33 import android.os.Handler; 34 import android.os.UserHandle; 35 import android.os.UserManager; 36 import android.provider.Settings; 37 import android.util.ArraySet; 38 import android.util.Log; 39 import android.util.Slog; 40 import android.util.SparseArray; 41 import android.view.Display; 42 import android.view.MotionEvent; 43 import android.widget.Toast; 44 45 import androidx.annotation.MainThread; 46 import androidx.annotation.VisibleForTesting; 47 48 import com.android.systemui.CoreStartable; 49 import com.android.systemui.R; 50 import com.android.systemui.car.CarServiceProvider; 51 import com.android.systemui.dagger.SysUISingleton; 52 import com.android.systemui.dagger.qualifiers.Main; 53 54 import java.io.PrintWriter; 55 import java.util.List; 56 import java.util.concurrent.atomic.AtomicReference; 57 import java.util.function.UnaryOperator; 58 59 import javax.inject.Inject; 60 61 /** 62 * Controls {@link DisplayInputSink}. It can be used for the display input lock or display input 63 * monitor. 64 * <ul> 65 * <li>For the display input lock, it observes for when the setting is changed and starts/stops 66 * display input lock window accordingly. 67 * <li>For the display input monitor, when the display turns off, it adds the spy window 68 * on the display to generate the user activity notification for the wake up.* 69 * </ul> 70 */ 71 @SysUISingleton 72 public final class DisplayInputSinkController implements CoreStartable { 73 private static final String TAG = "DisplayInputLock"; 74 // 4 displays would be enough for most systems. 75 private static final int INITIAL_INPUT_SINK_CAPACITY = 4; 76 static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); 77 78 private static final Uri DISPLAY_INPUT_LOCK_URI = 79 Settings.Global.getUriFor(CarSettings.Global.DISPLAY_INPUT_LOCK); 80 81 private final Context mContext; 82 83 private final CarServiceProvider mCarServiceProvider; 84 private final Handler mHandler; 85 private final DisplayManager mDisplayManager; 86 private final ContentObserver mSettingsObserver; 87 88 // Map of input sinks per display that are currently on going. (key: displayId) 89 private final SparseArray<DisplayInputSink> mDisplayInputSinks; 90 91 // Map of input locks that are currently on going. (key: displayId) 92 private final ArraySet<Integer> mDisplayInputLockedDisplays; 93 94 // A set of display unique ids from the display input lock setting. 95 private final ArraySet<String> mDisplayInputLockSetting; 96 97 // Map of the available passenger displays. (key: displayId) 98 private final SparseArray<Display> mPassengerDisplays; 99 100 private CarOccupantZoneManager mOccupantZoneManager; 101 private CarPowerManager mCarPowerManager; 102 103 @VisibleForTesting 104 final DisplayManager.DisplayListener mDisplayListener = 105 new DisplayManager.DisplayListener() { 106 @Override 107 @MainThread 108 public void onDisplayAdded(int displayId) { 109 mayUpdatePassengerDisplayOnAdded(displayId); 110 refreshDisplayInputSink(displayId, "onDisplayAdded"); 111 } 112 113 @Override 114 @MainThread 115 public void onDisplayRemoved(int displayId) { 116 if (!mPassengerDisplays.contains(displayId)) return; 117 mayStopDisplayInputLock(mDisplayManager.getDisplay(displayId)); 118 mayStopDisplayInputMonitor(displayId); 119 mPassengerDisplays.remove(displayId); 120 } 121 122 @Override 123 @MainThread 124 public void onDisplayChanged(int displayId) { 125 refreshDisplayInputSink(displayId, "onDisplayChanged"); 126 } 127 }; 128 refreshDisplayInputSink(int displayId, String caller)129 private void refreshDisplayInputSink(int displayId, String caller) { 130 int index = mPassengerDisplays.indexOfKey(displayId); 131 if (index < 0) { 132 if (DBG) Slog.d(TAG, caller + ": Not a passenger display#" + displayId); 133 return; 134 } 135 decideDisplayInputSink(index); 136 } 137 138 @Inject DisplayInputSinkController(Context context, @Main Handler handler, CarServiceProvider carServiceProvider)139 public DisplayInputSinkController(Context context, @Main Handler handler, 140 CarServiceProvider carServiceProvider) { 141 this(context, handler, carServiceProvider, 142 new SparseArray<DisplayInputSink>(INITIAL_INPUT_SINK_CAPACITY), 143 new ArraySet<Integer>(INITIAL_INPUT_SINK_CAPACITY), 144 new ArraySet<String>(INITIAL_INPUT_SINK_CAPACITY), 145 new SparseArray<Display>(INITIAL_INPUT_SINK_CAPACITY)); 146 } 147 148 @VisibleForTesting DisplayInputSinkController(Context context, @Main Handler handler, CarServiceProvider carServiceProvider, SparseArray<DisplayInputSink> displayInputSinks, ArraySet<Integer> displayInputLockedDisplays, ArraySet<String> displayInputLockSetting, SparseArray<Display> passengerDisplays)149 DisplayInputSinkController(Context context, @Main Handler handler, 150 CarServiceProvider carServiceProvider, 151 SparseArray<DisplayInputSink> displayInputSinks, 152 ArraySet<Integer> displayInputLockedDisplays, 153 ArraySet<String> displayInputLockSetting, 154 SparseArray<Display> passengerDisplays) { 155 mContext = context; 156 mHandler = handler; 157 mDisplayManager = mContext.getSystemService(DisplayManager.class); 158 mSettingsObserver = new ContentObserver(mHandler) { 159 @Override 160 @MainThread 161 public void onChange(boolean selfChange, Uri uri) { 162 if (DBG) Slog.d(TAG, "onChange: self=" + selfChange + ", uri=" + uri); 163 refreshDisplayInputLockSetting(); 164 } 165 }; 166 mCarServiceProvider = carServiceProvider; 167 168 mDisplayInputSinks = displayInputSinks; 169 mDisplayInputLockedDisplays = displayInputLockedDisplays; 170 mDisplayInputLockSetting = displayInputLockSetting; 171 mPassengerDisplays = passengerDisplays; 172 } 173 174 @Override start()175 public void start() { 176 if (UserHandle.myUserId() != UserHandle.USER_SYSTEM 177 && UserManager.isHeadlessSystemUserMode()) { 178 Slog.i(TAG, "Disable DisplayInputSinkController for non system user " 179 + UserHandle.myUserId()); 180 return; 181 } 182 183 mCarServiceProvider.addListener(mCarServiceOnConnectedListener); 184 mContext.getContentResolver().registerContentObserver(DISPLAY_INPUT_LOCK_URI, 185 /* notifyForDescendants= */ false, mSettingsObserver); 186 mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); 187 } 188 189 private final CarServiceProvider.CarServiceOnConnectedListener mCarServiceOnConnectedListener = 190 new CarServiceProvider.CarServiceOnConnectedListener() { 191 @Override 192 public void onConnected(Car car) { 193 mOccupantZoneManager = car.getCarManager(CarOccupantZoneManager.class); 194 mCarPowerManager = car.getCarManager(CarPowerManager.class); 195 initPassengerDisplays(); 196 refreshDisplayInputLockSetting(); 197 } 198 }; 199 200 // Assumes that all main displays for passengers are static. initPassengerDisplays()201 private void initPassengerDisplays() { 202 List<OccupantZoneInfo> allZones = mOccupantZoneManager.getAllOccupantZones(); 203 for (int i = allZones.size() - 1; i >= 0; --i) { 204 OccupantZoneInfo zone = allZones.get(i); 205 if (zone.occupantType == OCCUPANT_TYPE_DRIVER) continue; // Skip a driver. 206 Display display = mOccupantZoneManager.getDisplayForOccupant(zone, DISPLAY_TYPE_MAIN); 207 if (display == null) { 208 Slog.w(TAG, "Can't access the display of zone=" + zone); 209 continue; 210 } 211 mPassengerDisplays.put(display.getDisplayId(), display); 212 } 213 } 214 mayUpdatePassengerDisplayOnAdded(int displayId)215 private void mayUpdatePassengerDisplayOnAdded(int displayId) { 216 if (mPassengerDisplays.contains(displayId)) { 217 // Display is already added to the passenger display list. 218 return; 219 } 220 if (mOccupantZoneManager == null) { 221 Slog.w(TAG, "CarService isn't connected yet"); 222 return; 223 } 224 OccupantZoneInfo zone = mOccupantZoneManager.getOccupantZoneForDisplayId(displayId); 225 if (zone == null) { 226 Slog.w(TAG, "Can't find the zone info for display#" + displayId); 227 return; 228 } 229 if (zone.occupantType == OCCUPANT_TYPE_DRIVER) { 230 // Skip a driver display 231 return; 232 } 233 Display display = mOccupantZoneManager.getDisplayForOccupant(zone, DISPLAY_TYPE_MAIN); 234 if (display == null) { 235 Slog.w(TAG, "Can't access the display of zone=" + zone); 236 return; 237 } 238 mPassengerDisplays.put(displayId, display); 239 } 240 241 // Start/stop display input locks from the current global setting. 242 @VisibleForTesting refreshDisplayInputLockSetting()243 void refreshDisplayInputLockSetting() { 244 String settingValue = getDisplayInputLockSettingValue(); 245 parseDisplayInputLockSettingValue(CarSettings.Global.DISPLAY_INPUT_LOCK, settingValue); 246 if (DBG) { 247 Slog.d(TAG, "refreshDisplayInputLock: settingValue=" + settingValue); 248 } 249 for (int i = mPassengerDisplays.size() - 1; i >= 0; --i) { 250 decideDisplayInputSink(i); 251 } 252 } 253 decideDisplayInputSink(int index)254 private void decideDisplayInputSink(int index) { 255 int displayId = mPassengerDisplays.keyAt(index); 256 Display display = mPassengerDisplays.valueAt(index); 257 if (mDisplayInputLockSetting.contains(display.getUniqueId())) { 258 mayStopDisplayInputMonitor(displayId); 259 mayStartDisplayInputLock(display); 260 } else if (Display.isOffState(display.getState())) { 261 mayStopDisplayInputLock(display); 262 mayStartDisplayInputMonitor(display); 263 } else { 264 mayStopDisplayInputLock(display); 265 mayStopDisplayInputMonitor(displayId); 266 } 267 } 268 getDisplayInputLockSettingValue()269 private String getDisplayInputLockSettingValue() { 270 return Settings.Global.getString(mContext.getContentResolver(), 271 CarSettings.Global.DISPLAY_INPUT_LOCK); 272 } 273 parseDisplayInputLockSettingValue(@onNull String settingKey, @Nullable String value)274 private void parseDisplayInputLockSettingValue(@NonNull String settingKey, 275 @Nullable String value) { 276 mDisplayInputLockSetting.clear(); 277 if (value == null || value.isEmpty()) { 278 return; 279 } 280 281 String[] entries = value.split(","); 282 int numEntries = entries.length; 283 mDisplayInputLockSetting.ensureCapacity(numEntries); 284 for (int i = 0; i < numEntries; i++) { 285 String uniqueId = entries[i]; 286 if (findDisplayIdByUniqueId(uniqueId) == Display.INVALID_DISPLAY) { 287 Slog.w(TAG, "Invalid display id: " + uniqueId); 288 continue; 289 } 290 mDisplayInputLockSetting.add(uniqueId); 291 } 292 } 293 findDisplayIdByUniqueId(@onNull String displayUniqueId)294 private int findDisplayIdByUniqueId(@NonNull String displayUniqueId) { 295 for (int i = mPassengerDisplays.size() - 1; i >= 0; --i) { 296 Display display = mPassengerDisplays.valueAt(i); 297 if (displayUniqueId.equals(display.getUniqueId())) { 298 return display.getDisplayId(); 299 } 300 } 301 return Display.INVALID_DISPLAY; 302 } 303 isDisplayInputLockStarted(int displayId)304 private boolean isDisplayInputLockStarted(int displayId) { 305 return mDisplayInputLockedDisplays.contains(displayId); 306 } 307 isDisplayInputMonitorStarted(int displayId)308 private boolean isDisplayInputMonitorStarted(int displayId) { 309 return !isDisplayInputLockStarted(displayId) && mDisplayInputSinks.get(displayId) != null; 310 } 311 312 @VisibleForTesting mayStartDisplayInputLock(@onNull Display display)313 void mayStartDisplayInputLock(@NonNull Display display) { 314 int displayId = display.getDisplayId(); 315 if (isDisplayInputLockStarted(displayId)) { 316 // Already started input lock for the given display. 317 if (DBG) Slog.d(TAG, "Input lock is already started for display#" + displayId); 318 return; 319 } 320 321 Slog.i(TAG, "Start input lock for display " + displayId); 322 mDisplayInputLockedDisplays.add(displayId); 323 Context displayContext = mContext.createDisplayContext(display); 324 AtomicReference<Toast> toastRef = new AtomicReference<>(null); 325 UnaryOperator<Toast> cancelToast = (toast) -> { 326 toast.cancel(); 327 return toast; 328 }; 329 UnaryOperator<Toast> createToast = (toast) -> Toast.makeText(displayContext, 330 R.string.display_input_lock_text, Toast.LENGTH_SHORT); 331 DisplayInputSink.OnInputEventListener callback = (event) -> { 332 if (DBG) { 333 Slog.d(TAG, "Received input events while input is locked for display#" 334 + event.getDisplayId()); 335 } 336 if (mCarPowerManager != null) { 337 mCarPowerManager.notifyUserActivity(event.getDisplayId()); 338 } 339 Runnable r = () -> { 340 // MotionEvents for clicks are ACTION_DOWN + ACTION_UP 341 // Only capture one of those events so the Toast shows once per click 342 if (event instanceof MotionEvent 343 && ((MotionEvent) event).getAction() == MotionEvent.ACTION_DOWN) { 344 if (toastRef.get() != null) { 345 toastRef.updateAndGet(cancelToast); 346 } 347 toastRef.updateAndGet(createToast).show(); 348 } 349 }; 350 mHandler.post(r); 351 }; 352 mDisplayInputSinks.put(displayId, new DisplayInputSink(display, callback)); 353 // Now that the display input lock is started, let's inform the user of it. 354 mHandler.post(() -> Toast.makeText(displayContext, R.string.display_input_lock_started_text, 355 Toast.LENGTH_SHORT).show()); 356 357 } 358 mayStartDisplayInputMonitor(Display display)359 private void mayStartDisplayInputMonitor(Display display) { 360 int displayId = display.getDisplayId(); 361 if (isDisplayInputMonitorStarted(displayId)) { 362 // Already started input monitor for the given display. 363 if (DBG) Slog.d(TAG, "Input monitor is already started for display#" + displayId); 364 return; 365 } 366 367 Slog.i(TAG, "Start input monitor for display#" + displayId); 368 DisplayInputSink.OnInputEventListener callback = (event) -> { 369 if (DBG) { 370 Slog.d(TAG, "Received input events for monitored display#" 371 + event.getDisplayId()); 372 } 373 if (mCarPowerManager != null) { 374 mCarPowerManager.notifyUserActivity(event.getDisplayId()); 375 } 376 }; 377 mDisplayInputSinks.put(displayId, new DisplayInputSink(display, callback)); 378 } 379 380 @VisibleForTesting mayStopDisplayInputLock(Display display)381 void mayStopDisplayInputLock(Display display) { 382 int displayId = display.getDisplayId(); 383 if (!isDisplayInputLockStarted(displayId)) { 384 if (DBG) Slog.d(TAG, "There is no input lock started for display#" + displayId); 385 return; 386 } 387 Slog.i(TAG, "Stop input lock for display#" + displayId); 388 mHandler.post(() -> Toast.makeText(mContext.createDisplayContext(display), 389 R.string.display_input_lock_stopped_text, Toast.LENGTH_SHORT).show()); 390 removeDisplayInputSink(displayId); 391 mDisplayInputLockedDisplays.remove(displayId); 392 } 393 mayStopDisplayInputMonitor(int displayId)394 private void mayStopDisplayInputMonitor(int displayId) { 395 if (!isDisplayInputMonitorStarted(displayId)) { 396 if (DBG) Slog.d(TAG, "There is no input monitor started for display#" + displayId); 397 return; 398 } 399 Slog.i(TAG, "Stop input monitor for display#" + displayId); 400 removeDisplayInputSink(displayId); 401 } 402 removeDisplayInputSink(int displayId)403 private void removeDisplayInputSink(int displayId) { 404 int index = mDisplayInputSinks.indexOfKey(displayId); 405 if (index < 0) { 406 throw new IllegalStateException("Can't find the input sink for display#" + displayId); 407 } 408 DisplayInputSink inputLock = mDisplayInputSinks.valueAt(index); 409 inputLock.release(); 410 mDisplayInputSinks.removeAt(index); 411 } 412 413 @Override dump(@onNull PrintWriter pw, @NonNull String[] args)414 public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { 415 pw.println("DisplayInputSinks:"); 416 int size = mDisplayInputSinks.size(); 417 for (int i = 0; i < size; i++) { 418 DisplayInputSink inputSink = mDisplayInputSinks.valueAt(i); 419 pw.printf(" %d: %s\n", i, inputSink.toString()); 420 } 421 422 pw.println("DisplayInputLockedWindows:"); 423 size = mDisplayInputLockedDisplays.size(); 424 for (int i = 0; i < size; i++) { 425 pw.printf(" %s\n", mDisplayInputLockedDisplays.valueAt(i).toString()); 426 } 427 428 pw.printf("DisplayInputLockSetting: %s\n", mDisplayInputLockSetting); 429 pw.print("PassegnerDisplays: ["); 430 for (int i = mPassengerDisplays.size() - 1; i >= 0; --i) { 431 pw.print(mPassengerDisplays.keyAt(i)); 432 if (i > 0) pw.print(", "); 433 } 434 pw.println(']'); 435 } 436 } 437