1 /*
2  * Copyright (C) 2013 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 package com.android.keyguard;
17 
18 import android.app.Presentation;
19 import android.content.Context;
20 import android.graphics.Color;
21 import android.graphics.Rect;
22 import android.hardware.devicestate.DeviceStateManager;
23 import android.hardware.display.DisplayManager;
24 import android.media.MediaRouter;
25 import android.media.MediaRouter.RouteInfo;
26 import android.os.Bundle;
27 import android.os.Trace;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.util.SparseArray;
31 import android.view.Display;
32 import android.view.DisplayAddress;
33 import android.view.DisplayInfo;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.WindowManager;
37 
38 import androidx.annotation.Nullable;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
42 import com.android.systemui.R;
43 import com.android.systemui.dagger.SysUISingleton;
44 import com.android.systemui.dagger.qualifiers.Main;
45 import com.android.systemui.dagger.qualifiers.UiBackground;
46 import com.android.systemui.flags.FeatureFlags;
47 import com.android.systemui.flags.Flags;
48 import com.android.systemui.navigationbar.NavigationBarController;
49 import com.android.systemui.navigationbar.NavigationBarView;
50 import com.android.systemui.settings.DisplayTracker;
51 import com.android.systemui.statusbar.policy.KeyguardStateController;
52 
53 import dagger.Lazy;
54 
55 import java.util.concurrent.Executor;
56 
57 import javax.inject.Inject;
58 
59 
60 @SysUISingleton
61 public class KeyguardDisplayManager {
62     protected static final String TAG = "KeyguardDisplayManager";
63     private static final boolean DEBUG = KeyguardConstants.DEBUG;
64 
65     private MediaRouter mMediaRouter = null;
66     private final DisplayManager mDisplayService;
67     private final DisplayTracker mDisplayTracker;
68     private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
69     private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
70     private final ConnectedDisplayKeyguardPresentation.Factory
71             mConnectedDisplayKeyguardPresentationFactory;
72     private final FeatureFlags mFeatureFlags;
73     private final Context mContext;
74 
75     private boolean mShowing;
76     private final DisplayInfo mTmpDisplayInfo = new DisplayInfo();
77 
78     private final DeviceStateHelper mDeviceStateHelper;
79     private final KeyguardStateController mKeyguardStateController;
80 
81     private final SparseArray<Presentation> mPresentations = new SparseArray<>();
82 
83     private final DisplayTracker.Callback mDisplayCallback =
84             new DisplayTracker.Callback() {
85                 @Override
86                 public void onDisplayAdded(int displayId) {
87                     Trace.beginSection(
88                             "KeyguardDisplayManager#onDisplayAdded(displayId=" + displayId + ")");
89                     final Display display = mDisplayService.getDisplay(displayId);
90                     if (mShowing) {
91                         updateNavigationBarVisibility(displayId, false /* navBarVisible */);
92                         showPresentation(display);
93                     }
94                     Trace.endSection();
95                 }
96 
97                 @Override
98                 public void onDisplayRemoved(int displayId) {
99                     Trace.beginSection(
100                             "KeyguardDisplayManager#onDisplayRemoved(displayId=" + displayId + ")");
101                     hidePresentation(displayId);
102                     Trace.endSection();
103                 }
104             };
105 
106     @Inject
KeyguardDisplayManager(Context context, Lazy<NavigationBarController> navigationBarControllerLazy, KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory, DisplayTracker displayTracker, @Main Executor mainExecutor, @UiBackground Executor uiBgExecutor, DeviceStateHelper deviceStateHelper, KeyguardStateController keyguardStateController, ConnectedDisplayKeyguardPresentation.Factory connectedDisplayKeyguardPresentationFactory, FeatureFlags featureFlags)107     public KeyguardDisplayManager(Context context,
108             Lazy<NavigationBarController> navigationBarControllerLazy,
109             KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
110             DisplayTracker displayTracker,
111             @Main Executor mainExecutor,
112             @UiBackground Executor uiBgExecutor,
113             DeviceStateHelper deviceStateHelper,
114             KeyguardStateController keyguardStateController,
115             ConnectedDisplayKeyguardPresentation.Factory
116                     connectedDisplayKeyguardPresentationFactory,
117             FeatureFlags featureFlags) {
118         mContext = context;
119         mNavigationBarControllerLazy = navigationBarControllerLazy;
120         mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
121         uiBgExecutor.execute(() -> mMediaRouter = mContext.getSystemService(MediaRouter.class));
122         mDisplayService = mContext.getSystemService(DisplayManager.class);
123         mDisplayTracker = displayTracker;
124         mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor);
125         mDeviceStateHelper = deviceStateHelper;
126         mKeyguardStateController = keyguardStateController;
127         mConnectedDisplayKeyguardPresentationFactory = connectedDisplayKeyguardPresentationFactory;
128         mFeatureFlags = featureFlags;
129     }
130 
isKeyguardShowable(Display display)131     private boolean isKeyguardShowable(Display display) {
132         if (display == null) {
133             if (DEBUG) Log.i(TAG, "Cannot show Keyguard on null display");
134             return false;
135         }
136         if (display.getDisplayId() == mDisplayTracker.getDefaultDisplayId()) {
137             if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on the default display");
138             return false;
139         }
140         display.getDisplayInfo(mTmpDisplayInfo);
141         if ((mTmpDisplayInfo.flags & Display.FLAG_PRIVATE) != 0) {
142             if (DEBUG) Log.i(TAG, "Do not show KeyguardPresentation on a private display");
143             return false;
144         }
145         if ((mTmpDisplayInfo.flags & Display.FLAG_ALWAYS_UNLOCKED) != 0) {
146             if (DEBUG) {
147                 Log.i(TAG, "Do not show KeyguardPresentation on an unlocked display");
148             }
149             return false;
150         }
151         if (mKeyguardStateController.isOccluded()
152                 && mDeviceStateHelper.isConcurrentDisplayActive(display)) {
153             if (DEBUG) {
154                 // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the
155                 // Keyguard state becomes "occluded". In this case, we should not show the
156                 // KeyguardPresentation, since the activity is presenting content onto the
157                 // non-default display.
158                 Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent"
159                         + " display is active");
160             }
161             return false;
162         }
163 
164         return true;
165     }
166     /**
167      * @param display The display to show the presentation on.
168      * @return {@code true} if a presentation was added.
169      *         {@code false} if the presentation cannot be added on that display or the presentation
170      *         was already there.
171      */
showPresentation(Display display)172     private boolean showPresentation(Display display) {
173         if (!isKeyguardShowable(display)) return false;
174         if (DEBUG) Log.i(TAG, "Keyguard enabled on display: " + display);
175         final int displayId = display.getDisplayId();
176         Presentation presentation = mPresentations.get(displayId);
177         if (presentation == null) {
178             final Presentation newPresentation = createPresentation(display);
179             newPresentation.setOnDismissListener(dialog -> {
180                 if (newPresentation.equals(mPresentations.get(displayId))) {
181                     mPresentations.remove(displayId);
182                 }
183             });
184             presentation = newPresentation;
185             try {
186                 presentation.show();
187             } catch (WindowManager.InvalidDisplayException ex) {
188                 Log.w(TAG, "Invalid display:", ex);
189                 presentation = null;
190             }
191             if (presentation != null) {
192                 mPresentations.append(displayId, presentation);
193                 return true;
194             }
195         }
196         return false;
197     }
198 
createPresentation(Display display)199     Presentation createPresentation(Display display) {
200         if (mFeatureFlags.isEnabled(Flags.ENABLE_CLOCK_KEYGUARD_PRESENTATION)) {
201             return mConnectedDisplayKeyguardPresentationFactory.create(display);
202         } else {
203             return new KeyguardPresentation(mContext, display, mKeyguardStatusViewComponentFactory);
204         }
205     }
206 
207     /**
208      * @param displayId The id of the display to hide the presentation off.
209      */
hidePresentation(int displayId)210     private void hidePresentation(int displayId) {
211         final Presentation presentation = mPresentations.get(displayId);
212         if (presentation != null) {
213             presentation.dismiss();
214             mPresentations.remove(displayId);
215         }
216     }
217 
show()218     public void show() {
219         if (!mShowing) {
220             if (DEBUG) Log.v(TAG, "show");
221             if (mMediaRouter != null) {
222                 mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY,
223                         mMediaRouterCallback, MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
224             } else {
225                 Log.w(TAG, "MediaRouter not yet initialized");
226             }
227             updateDisplays(true /* showing */);
228         }
229         mShowing = true;
230     }
231 
hide()232     public void hide() {
233         if (mShowing) {
234             if (DEBUG) Log.v(TAG, "hide");
235             if (mMediaRouter != null) {
236                 mMediaRouter.removeCallback(mMediaRouterCallback);
237             }
238             updateDisplays(false /* showing */);
239         }
240         mShowing = false;
241     }
242 
243     private final MediaRouter.SimpleCallback mMediaRouterCallback =
244             new MediaRouter.SimpleCallback() {
245         @Override
246         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
247             if (DEBUG) Log.d(TAG, "onRouteSelected: type=" + type + ", info=" + info);
248             updateDisplays(mShowing);
249         }
250 
251         @Override
252         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
253             if (DEBUG) Log.d(TAG, "onRouteUnselected: type=" + type + ", info=" + info);
254             updateDisplays(mShowing);
255         }
256 
257         @Override
258         public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) {
259             if (DEBUG) Log.d(TAG, "onRoutePresentationDisplayChanged: info=" + info);
260             updateDisplays(mShowing);
261         }
262     };
263 
updateDisplays(boolean showing)264     protected boolean updateDisplays(boolean showing) {
265         boolean changed = false;
266         if (showing) {
267             final Display[] displays = mDisplayTracker.getAllDisplays();
268             for (Display display : displays) {
269                 int displayId = display.getDisplayId();
270                 updateNavigationBarVisibility(displayId, false /* navBarVisible */);
271                 changed |= showPresentation(display);
272             }
273         } else {
274             changed = mPresentations.size() > 0;
275             for (int i = mPresentations.size() - 1; i >= 0; i--) {
276                 int displayId = mPresentations.keyAt(i);
277                 updateNavigationBarVisibility(displayId, true /* navBarVisible */);
278                 mPresentations.valueAt(i).dismiss();
279             }
280             mPresentations.clear();
281         }
282         return changed;
283     }
284 
285     // TODO(b/127878649): this logic is from
286     //  {@link StatusBarKeyguardViewManager#updateNavigationBarVisibility}. Try to revisit a long
287     //  term solution in R.
updateNavigationBarVisibility(int displayId, boolean navBarVisible)288     private void updateNavigationBarVisibility(int displayId, boolean navBarVisible) {
289         // Leave this task to {@link StatusBarKeyguardViewManager}
290         if (displayId == mDisplayTracker.getDefaultDisplayId()) return;
291 
292         NavigationBarView navBarView = mNavigationBarControllerLazy.get()
293                 .getNavigationBarView(displayId);
294         // We may not have nav bar on a display.
295         if (navBarView == null) return;
296 
297         if (navBarVisible) {
298             navBarView.getRootView().setVisibility(View.VISIBLE);
299         } else {
300             navBarView.getRootView().setVisibility(View.GONE);
301         }
302 
303     }
304 
305     /**
306      * Helper used to receive device state info from {@link DeviceStateManager}.
307      */
308     static class DeviceStateHelper implements DeviceStateManager.DeviceStateCallback {
309 
310         @Nullable
311         private final DisplayAddress.Physical mRearDisplayPhysicalAddress;
312 
313         // TODO(b/271317597): These device states should be defined in DeviceStateManager
314         private final int mConcurrentState;
315         private boolean mIsInConcurrentDisplayState;
316 
317         @Inject
DeviceStateHelper(Context context, DeviceStateManager deviceStateManager, @Main Executor mainExecutor)318         DeviceStateHelper(Context context,
319                 DeviceStateManager deviceStateManager,
320                 @Main Executor mainExecutor) {
321 
322             final String rearDisplayPhysicalAddress = context.getResources().getString(
323                     com.android.internal.R.string.config_rearDisplayPhysicalAddress);
324             if (TextUtils.isEmpty(rearDisplayPhysicalAddress)) {
325                 mRearDisplayPhysicalAddress = null;
326             } else {
327                 mRearDisplayPhysicalAddress = DisplayAddress
328                         .fromPhysicalDisplayId(Long.parseLong(rearDisplayPhysicalAddress));
329             }
330 
331             mConcurrentState = context.getResources().getInteger(
332                     com.android.internal.R.integer.config_deviceStateConcurrentRearDisplay);
333             deviceStateManager.registerCallback(mainExecutor, this);
334         }
335 
336         @Override
onStateChanged(int state)337         public void onStateChanged(int state) {
338             // When concurrent state ends, the display also turns off. This is enforced in various
339             // ExtensionRearDisplayPresentationTest CTS tests. So, we don't need to invoke
340             // hide() since that will happen through the onDisplayRemoved callback.
341             mIsInConcurrentDisplayState = state == mConcurrentState;
342         }
343 
isConcurrentDisplayActive(Display display)344         boolean isConcurrentDisplayActive(Display display) {
345             return mIsInConcurrentDisplayState
346                     && mRearDisplayPhysicalAddress != null
347                     && mRearDisplayPhysicalAddress.equals(display.getAddress());
348         }
349     }
350 
351 
352     @VisibleForTesting
353     static final class KeyguardPresentation extends Presentation {
354         private static final int VIDEO_SAFE_REGION = 80; // Percentage of display width & height
355         private static final int MOVE_CLOCK_TIMEOUT = 10000; // 10s
356         private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
357         private KeyguardClockSwitchController mKeyguardClockSwitchController;
358         private View mClock;
359         private int mUsableWidth;
360         private int mUsableHeight;
361         private int mMarginTop;
362         private int mMarginLeft;
363         Runnable mMoveTextRunnable = new Runnable() {
364             @Override
365             public void run() {
366                 int x = mMarginLeft + (int) (Math.random() * (mUsableWidth - mClock.getWidth()));
367                 int y = mMarginTop + (int) (Math.random() * (mUsableHeight - mClock.getHeight()));
368                 mClock.setTranslationX(x);
369                 mClock.setTranslationY(y);
370                 mClock.postDelayed(mMoveTextRunnable, MOVE_CLOCK_TIMEOUT);
371             }
372         };
373 
KeyguardPresentation(Context context, Display display, KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory)374         KeyguardPresentation(Context context, Display display,
375                 KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory) {
376             super(context, display, R.style.Theme_SystemUI_KeyguardPresentation,
377                     WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
378             mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
379             setCancelable(false);
380         }
381 
382         @Override
cancel()383         public void cancel() {
384             // Do not allow anything to cancel KeyguardPresentation except KeyguardDisplayManager.
385         }
386 
387         @Override
onDetachedFromWindow()388         public void onDetachedFromWindow() {
389             mClock.removeCallbacks(mMoveTextRunnable);
390         }
391 
392         @Override
onDisplayChanged()393         public void onDisplayChanged() {
394             updateBounds();
395             getWindow().getDecorView().requestLayout();
396         }
397 
398         @Override
onCreate(Bundle savedInstanceState)399         protected void onCreate(Bundle savedInstanceState) {
400             super.onCreate(savedInstanceState);
401 
402             updateBounds();
403 
404             setContentView(LayoutInflater.from(getContext())
405                     .inflate(R.layout.keyguard_presentation, null));
406 
407             // Logic to make the lock screen fullscreen
408             getWindow().getDecorView().setSystemUiVisibility(
409                     View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
410                             | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
411                             | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
412             getWindow().getAttributes().setFitInsetsTypes(0 /* types */);
413             getWindow().setNavigationBarContrastEnforced(false);
414             getWindow().setNavigationBarColor(Color.TRANSPARENT);
415 
416             mClock = findViewById(R.id.clock);
417 
418             // Avoid screen burn in
419             mClock.post(mMoveTextRunnable);
420 
421             mKeyguardClockSwitchController = mKeyguardStatusViewComponentFactory
422                     .build(findViewById(R.id.clock))
423                     .getKeyguardClockSwitchController();
424 
425             mKeyguardClockSwitchController.setOnlyClock(true);
426             mKeyguardClockSwitchController.init();
427         }
428 
updateBounds()429         private void updateBounds() {
430             final Rect bounds = getWindow().getWindowManager().getMaximumWindowMetrics()
431                     .getBounds();
432             mUsableWidth = VIDEO_SAFE_REGION * bounds.width() / 100;
433             mUsableHeight = VIDEO_SAFE_REGION * bounds.height() / 100;
434             mMarginLeft = (100 - VIDEO_SAFE_REGION) * bounds.width() / 200;
435             mMarginTop = (100 - VIDEO_SAFE_REGION) * bounds.height() / 200;
436         }
437     }
438 }
439