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