1 /*
2  * Copyright (C) 2018 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.server.wm;
18 
19 import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED;
20 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
21 import static android.view.Display.DEFAULT_DISPLAY;
22 import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
24 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
25 import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
26 
27 import android.animation.ArgbEvaluator;
28 import android.animation.ValueAnimator;
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.app.ActivityManager;
32 import android.app.ActivityThread;
33 import android.content.BroadcastReceiver;
34 import android.content.Context;
35 import android.content.Intent;
36 import android.content.IntentFilter;
37 import android.graphics.Insets;
38 import android.graphics.PixelFormat;
39 import android.graphics.Rect;
40 import android.graphics.drawable.ColorDrawable;
41 import android.os.Binder;
42 import android.os.Bundle;
43 import android.os.Handler;
44 import android.os.IBinder;
45 import android.os.Looper;
46 import android.os.Message;
47 import android.os.UserHandle;
48 import android.os.UserManager;
49 import android.provider.Settings;
50 import android.util.DisplayMetrics;
51 import android.util.Slog;
52 import android.view.Display;
53 import android.view.Gravity;
54 import android.view.MotionEvent;
55 import android.view.View;
56 import android.view.ViewGroup;
57 import android.view.ViewTreeObserver;
58 import android.view.WindowInsets;
59 import android.view.WindowInsets.Type;
60 import android.view.WindowManager;
61 import android.view.animation.AnimationUtils;
62 import android.view.animation.Interpolator;
63 import android.widget.Button;
64 import android.widget.FrameLayout;
65 import android.widget.RelativeLayout;
66 
67 import com.android.internal.R;
68 
69 /**
70  *  Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
71  *  entering immersive mode.
72  */
73 public class ImmersiveModeConfirmation {
74     private static final String TAG = "ImmersiveModeConfirmation";
75     private static final boolean DEBUG = false;
76     private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
77     private static final String CONFIRMED = "confirmed";
78     private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE =
79             WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
80 
81     private static boolean sConfirmed;
82 
83     private final Context mContext;
84     private final H mHandler;
85     private final long mShowDelayMs;
86     private final long mPanicThresholdMs;
87     private final IBinder mWindowToken = new Binder();
88 
89     private ClingWindowView mClingWindow;
90     private long mPanicTime;
91     /** The last {@link WindowManager} that is used to add the confirmation window. */
92     @Nullable
93     private WindowManager mWindowManager;
94     /**
95      * The WindowContext that is registered with {@link #mWindowManager} with options to specify the
96      * {@link RootDisplayArea} to attach the confirmation window.
97      */
98     @Nullable
99     private Context mWindowContext;
100     /**
101      * The root display area feature id that the {@link #mWindowContext} is attaching to.
102      */
103     private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED;
104     // Local copy of vr mode enabled state, to avoid calling into VrManager with
105     // the lock held.
106     private boolean mVrModeEnabled;
107     private boolean mCanSystemBarsBeShownByUser;
108     private int mLockTaskState = LOCK_TASK_MODE_NONE;
109 
ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled, boolean canSystemBarsBeShownByUser)110     ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled,
111             boolean canSystemBarsBeShownByUser) {
112         final Display display = context.getDisplay();
113         final Context uiContext = ActivityThread.currentActivityThread().getSystemUiContext();
114         mContext = display.getDisplayId() == DEFAULT_DISPLAY
115                 ? uiContext : uiContext.createDisplayContext(display);
116         mHandler = new H(looper);
117         mShowDelayMs = context.getResources().getInteger(R.integer.dock_enter_exit_duration) * 3L;
118         mPanicThresholdMs = context.getResources()
119                 .getInteger(R.integer.config_immersive_mode_confirmation_panic);
120         mVrModeEnabled = vrModeEnabled;
121         mCanSystemBarsBeShownByUser = canSystemBarsBeShownByUser;
122     }
123 
loadSetting(int currentUserId, Context context)124     static boolean loadSetting(int currentUserId, Context context) {
125         final boolean wasConfirmed = sConfirmed;
126         sConfirmed = false;
127         if (DEBUG) Slog.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
128         String value = null;
129         try {
130             value = Settings.Secure.getStringForUser(context.getContentResolver(),
131                     Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
132                     UserHandle.USER_CURRENT);
133             sConfirmed = CONFIRMED.equals(value);
134             if (DEBUG) Slog.d(TAG, "Loaded sConfirmed=" + sConfirmed);
135         } catch (Throwable t) {
136             Slog.w(TAG, "Error loading confirmations, value=" + value, t);
137         }
138         return sConfirmed != wasConfirmed;
139     }
140 
saveSetting(Context context)141     private static void saveSetting(Context context) {
142         if (DEBUG) Slog.d(TAG, "saveSetting()");
143         try {
144             final String value = sConfirmed ? CONFIRMED : null;
145             Settings.Secure.putStringForUser(context.getContentResolver(),
146                     Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
147                     value,
148                     UserHandle.USER_CURRENT);
149             if (DEBUG) Slog.d(TAG, "Saved value=" + value);
150         } catch (Throwable t) {
151             Slog.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
152         }
153     }
154 
release()155     void release() {
156         mHandler.removeMessages(H.SHOW);
157         mHandler.removeMessages(H.HIDE);
158     }
159 
onSettingChanged(int currentUserId)160     boolean onSettingChanged(int currentUserId) {
161         final boolean changed = loadSetting(currentUserId, mContext);
162         // Remove the window if the setting changes to be confirmed.
163         if (changed && sConfirmed) {
164             mHandler.sendEmptyMessage(H.HIDE);
165         }
166         return changed;
167     }
168 
immersiveModeChangedLw(int rootDisplayAreaId, boolean isImmersiveMode, boolean userSetupComplete, boolean navBarEmpty)169     void immersiveModeChangedLw(int rootDisplayAreaId, boolean isImmersiveMode,
170             boolean userSetupComplete, boolean navBarEmpty) {
171         mHandler.removeMessages(H.SHOW);
172         if (isImmersiveMode) {
173             if (DEBUG) Slog.d(TAG, "immersiveModeChanged() sConfirmed=" +  sConfirmed);
174             if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed)
175                     && userSetupComplete
176                     && !mVrModeEnabled
177                     && mCanSystemBarsBeShownByUser
178                     && !navBarEmpty
179                     && !UserManager.isDeviceInDemoMode(mContext)
180                     && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
181                 final Message msg = mHandler.obtainMessage(H.SHOW);
182                 msg.arg1 = rootDisplayAreaId;
183                 mHandler.sendMessageDelayed(msg, mShowDelayMs);
184             }
185         } else {
186             mHandler.sendEmptyMessage(H.HIDE);
187         }
188     }
189 
onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode, boolean navBarEmpty)190     boolean onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode,
191             boolean navBarEmpty) {
192         if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) {
193             // turning the screen back on within the panic threshold
194             return mClingWindow == null;
195         }
196         if (isScreenOn && inImmersiveMode && !navBarEmpty) {
197             // turning the screen off, remember if we were in immersive mode
198             mPanicTime = time;
199         } else {
200             mPanicTime = 0;
201         }
202         return false;
203     }
204 
confirmCurrentPrompt()205     void confirmCurrentPrompt() {
206         if (mClingWindow != null) {
207             if (DEBUG) Slog.d(TAG, "confirmCurrentPrompt()");
208             mHandler.post(mConfirm);
209         }
210     }
211 
handleHide()212     private void handleHide() {
213         if (mClingWindow != null) {
214             if (DEBUG) Slog.d(TAG, "Hiding immersive mode confirmation");
215             if (mWindowManager != null) {
216                 try {
217                     mWindowManager.removeView(mClingWindow);
218                 } catch (WindowManager.InvalidDisplayException e) {
219                     Slog.w(TAG, "Fail to hide the immersive confirmation window because of "
220                             + e);
221                 }
222                 mWindowManager = null;
223                 mWindowContext = null;
224             }
225             mClingWindow = null;
226         }
227     }
228 
getClingWindowLayoutParams()229     private WindowManager.LayoutParams getClingWindowLayoutParams() {
230         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
231                 ViewGroup.LayoutParams.MATCH_PARENT,
232                 ViewGroup.LayoutParams.MATCH_PARENT,
233                 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE,
234                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
235                         | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
236                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
237                 PixelFormat.TRANSLUCENT);
238         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
239         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
240         // Trusted overlay so touches outside the touchable area are allowed to pass through
241         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
242                 | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
243                 | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
244         lp.setTitle("ImmersiveModeConfirmation");
245         lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
246         lp.token = getWindowToken();
247         return lp;
248     }
249 
getBubbleLayoutParams()250     private FrameLayout.LayoutParams getBubbleLayoutParams() {
251         return new FrameLayout.LayoutParams(
252                 getClingWindowWidth(),
253                 ViewGroup.LayoutParams.WRAP_CONTENT,
254                 Gravity.CENTER_HORIZONTAL | Gravity.TOP);
255     }
256 
257     /**
258      * Returns the width of the cling window.
259      */
getClingWindowWidth()260     private int getClingWindowWidth() {
261         return mContext.getResources().getDimensionPixelSize(
262                 R.dimen.immersive_mode_cling_width);
263     }
264 
265     /**
266      * @return the window token that's used by all ImmersiveModeConfirmation windows.
267      */
getWindowToken()268     IBinder getWindowToken() {
269         return mWindowToken;
270     }
271 
272     private class ClingWindowView extends FrameLayout {
273         private static final int BGCOLOR = 0x80000000;
274         private static final int OFFSET_DP = 96;
275         private static final int ANIMATION_DURATION = 250;
276 
277         private final Runnable mConfirm;
278         private final ColorDrawable mColor = new ColorDrawable(0);
279         private final Interpolator mInterpolator;
280         private ValueAnimator mColorAnim;
281         private ViewGroup mClingLayout;
282 
283         private Runnable mUpdateLayoutRunnable = new Runnable() {
284             @Override
285             public void run() {
286                 if (mClingLayout != null && mClingLayout.getParent() != null) {
287                     mClingLayout.setLayoutParams(getBubbleLayoutParams());
288                 }
289             }
290         };
291 
292         private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
293                 new ViewTreeObserver.OnComputeInternalInsetsListener() {
294                     private final int[] mTmpInt2 = new int[2];
295 
296                     @Override
297                     public void onComputeInternalInsets(
298                             ViewTreeObserver.InternalInsetsInfo inoutInfo) {
299                         // Set touchable region to cover the cling layout.
300                         mClingLayout.getLocationInWindow(mTmpInt2);
301                         inoutInfo.setTouchableInsets(
302                                 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
303                         inoutInfo.touchableRegion.set(
304                                 mTmpInt2[0],
305                                 mTmpInt2[1],
306                                 mTmpInt2[0] + mClingLayout.getWidth(),
307                                 mTmpInt2[1] + mClingLayout.getHeight());
308                     }
309                 };
310 
311         private BroadcastReceiver mReceiver = new BroadcastReceiver() {
312             @Override
313             public void onReceive(Context context, Intent intent) {
314                 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
315                     post(mUpdateLayoutRunnable);
316                 }
317             }
318         };
319 
ClingWindowView(Context context, Runnable confirm)320         ClingWindowView(Context context, Runnable confirm) {
321             super(context);
322             mConfirm = confirm;
323             setBackground(mColor);
324             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
325             mInterpolator = AnimationUtils
326                     .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
327         }
328 
329         @Override
onAttachedToWindow()330         public void onAttachedToWindow() {
331             super.onAttachedToWindow();
332 
333             DisplayMetrics metrics = new DisplayMetrics();
334             mContext.getDisplay().getMetrics(metrics);
335             float density = metrics.density;
336 
337             getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
338 
339             // create the confirmation cling
340             mClingLayout = (ViewGroup)
341                     View.inflate(getContext(), R.layout.immersive_mode_cling, null);
342 
343             final Button ok = mClingLayout.findViewById(R.id.ok);
344             ok.setOnClickListener(new OnClickListener() {
345                 @Override
346                 public void onClick(View v) {
347                     mConfirm.run();
348                 }
349             });
350             addView(mClingLayout, getBubbleLayoutParams());
351 
352             if (ActivityManager.isHighEndGfx()) {
353                 final View cling = mClingLayout;
354                 cling.setAlpha(0f);
355                 cling.setTranslationY(-OFFSET_DP * density);
356 
357                 postOnAnimation(new Runnable() {
358                     @Override
359                     public void run() {
360                         cling.animate()
361                                 .alpha(1f)
362                                 .translationY(0)
363                                 .setDuration(ANIMATION_DURATION)
364                                 .setInterpolator(mInterpolator)
365                                 .withLayer()
366                                 .start();
367 
368                         mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
369                         mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
370                             @Override
371                             public void onAnimationUpdate(ValueAnimator animation) {
372                                 final int c = (Integer) animation.getAnimatedValue();
373                                 mColor.setColor(c);
374                             }
375                         });
376                         mColorAnim.setDuration(ANIMATION_DURATION);
377                         mColorAnim.setInterpolator(mInterpolator);
378                         mColorAnim.start();
379                     }
380                 });
381             } else {
382                 mColor.setColor(BGCOLOR);
383             }
384 
385             mContext.registerReceiver(mReceiver,
386                     new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
387         }
388 
389         @Override
onDetachedFromWindow()390         public void onDetachedFromWindow() {
391             mContext.unregisterReceiver(mReceiver);
392         }
393 
394         @Override
onTouchEvent(MotionEvent motion)395         public boolean onTouchEvent(MotionEvent motion) {
396             return true;
397         }
398 
399         @Override
onApplyWindowInsets(WindowInsets insets)400         public WindowInsets onApplyWindowInsets(WindowInsets insets) {
401             // If the top display cutout overlaps with the full-width (windowWidth=-1)/centered
402             // dialog, then adjust the dialog contents by the cutout
403             final int width = getWidth();
404             final int windowWidth = getClingWindowWidth();
405             final Rect topDisplayCutout = insets.getDisplayCutout() != null
406                     ? insets.getDisplayCutout().getBoundingRectTop()
407                     : new Rect();
408             final boolean intersectsTopCutout = topDisplayCutout.intersects(
409                     width - (windowWidth / 2), 0,
410                     width + (windowWidth / 2), topDisplayCutout.bottom);
411             if (mClingWindow != null &&
412                     (windowWidth < 0 || (width > 0 && intersectsTopCutout))) {
413                 final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon);
414                 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)
415                         iconView.getLayoutParams();
416                 lp.topMargin = topDisplayCutout.bottom;
417                 iconView.setLayoutParams(lp);
418             }
419             // we will be hiding the nav bar, so layout as if it's already hidden
420             return new WindowInsets.Builder(insets).setInsets(
421                     Type.systemBars(), Insets.NONE).build();
422         }
423     }
424 
425     /**
426      * DO HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD
427      * The reason why we add this method is to avoid the deadlock of WMG->WMS and WMS->WMG
428      * when ImmersiveModeConfirmation object is created.
429      *
430      * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the
431      *         confirmation window.
432      */
433     @NonNull
createWindowManager(int rootDisplayAreaId)434     private WindowManager createWindowManager(int rootDisplayAreaId) {
435         if (mWindowManager != null) {
436             throw new IllegalStateException(
437                     "Must not create a new WindowManager while there is an existing one");
438         }
439         // Create window context to specify the RootDisplayArea
440         final Bundle options = getOptionsForWindowContext(rootDisplayAreaId);
441         mWindowContextRootDisplayAreaId = rootDisplayAreaId;
442         mWindowContext = mContext.createWindowContext(
443                 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
444         mWindowManager = mWindowContext.getSystemService(WindowManager.class);
445         return mWindowManager;
446     }
447 
448     /**
449      * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window.
450      *         {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}.
451      */
452     @Nullable
getOptionsForWindowContext(int rootDisplayAreaId)453     private Bundle getOptionsForWindowContext(int rootDisplayAreaId) {
454         // In case we don't care which root display area the window manager is specifying.
455         if (rootDisplayAreaId == FEATURE_UNDEFINED) {
456             return null;
457         }
458 
459         final Bundle options = new Bundle();
460         options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
461         return options;
462     }
463 
handleShow(int rootDisplayAreaId)464     private void handleShow(int rootDisplayAreaId) {
465         if (mClingWindow != null) {
466             if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) {
467                 if (DEBUG) Slog.d(TAG, "Immersive mode confirmation has already been shown");
468                 return;
469             } else {
470                 // Hide the existing confirmation before show a new one in the new root.
471                 if (DEBUG) Slog.d(TAG, "Immersive mode confirmation was shown in a different root");
472                 handleHide();
473             }
474         }
475 
476         if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation");
477         mClingWindow = new ClingWindowView(mContext, mConfirm);
478         // show the confirmation
479         final WindowManager.LayoutParams lp = getClingWindowLayoutParams();
480         try {
481             createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp);
482         } catch (WindowManager.InvalidDisplayException e) {
483             Slog.w(TAG, "Fail to show the immersive confirmation window because of " + e);
484         }
485     }
486 
487     private final Runnable mConfirm = new Runnable() {
488         @Override
489         public void run() {
490             if (DEBUG) Slog.d(TAG, "mConfirm.run()");
491             if (!sConfirmed) {
492                 sConfirmed = true;
493                 saveSetting(mContext);
494             }
495             handleHide();
496         }
497     };
498 
499     private final class H extends Handler {
500         private static final int SHOW = 1;
501         private static final int HIDE = 2;
502 
H(Looper looper)503         H(Looper looper) {
504             super(looper);
505         }
506 
507         @Override
handleMessage(Message msg)508         public void handleMessage(Message msg) {
509             if (CLIENT_TRANSIENT) {
510                 return;
511             }
512             switch(msg.what) {
513                 case SHOW:
514                     handleShow(msg.arg1);
515                     break;
516                 case HIDE:
517                     handleHide();
518                     break;
519             }
520         }
521     }
522 
onVrStateChangedLw(boolean enabled)523     void onVrStateChangedLw(boolean enabled) {
524         mVrModeEnabled = enabled;
525         if (mVrModeEnabled) {
526             mHandler.removeMessages(H.SHOW);
527             mHandler.sendEmptyMessage(H.HIDE);
528         }
529     }
530 
onLockTaskModeChangedLw(int lockTaskState)531     void onLockTaskModeChangedLw(int lockTaskState) {
532         mLockTaskState = lockTaskState;
533     }
534 }
535