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