• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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