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 
23 import android.animation.ArgbEvaluator;
24 import android.animation.ValueAnimator;
25 import android.app.ActivityManager;
26 import android.app.ActivityThread;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.graphics.PixelFormat;
32 import android.graphics.drawable.ColorDrawable;
33 import android.os.Binder;
34 import android.os.Handler;
35 import android.os.IBinder;
36 import android.os.Looper;
37 import android.os.Message;
38 import android.os.UserHandle;
39 import android.os.UserManager;
40 import android.provider.Settings;
41 import android.util.DisplayMetrics;
42 import android.util.Slog;
43 import android.view.Display;
44 import android.view.Gravity;
45 import android.view.MotionEvent;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.view.ViewTreeObserver;
49 import android.view.WindowInsets.Type;
50 import android.view.WindowManager;
51 import android.view.animation.Animation;
52 import android.view.animation.AnimationUtils;
53 import android.view.animation.Interpolator;
54 import android.widget.Button;
55 import android.widget.FrameLayout;
56 
57 import com.android.internal.R;
58 
59 /**
60  *  Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
61  *  entering immersive mode.
62  */
63 public class ImmersiveModeConfirmation {
64     private static final String TAG = "ImmersiveModeConfirmation";
65     private static final boolean DEBUG = false;
66     private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
67     private static final String CONFIRMED = "confirmed";
68 
69     private static boolean sConfirmed;
70 
71     private final Context mContext;
72     private final H mHandler;
73     private final long mShowDelayMs;
74     private final long mPanicThresholdMs;
75     private final IBinder mWindowToken = new Binder();
76 
77     private ClingWindowView mClingWindow;
78     private long mPanicTime;
79     private WindowManager mWindowManager;
80     // Local copy of vr mode enabled state, to avoid calling into VrManager with
81     // the lock held.
82     private boolean mVrModeEnabled;
83     private int mLockTaskState = LOCK_TASK_MODE_NONE;
84 
ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled)85     ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled) {
86         final Display display = context.getDisplay();
87         final Context uiContext = ActivityThread.currentActivityThread().getSystemUiContext();
88         mContext = display.getDisplayId() == DEFAULT_DISPLAY
89                 ? uiContext : uiContext.createDisplayContext(display);
90         mHandler = new H(looper);
91         mShowDelayMs = getNavBarExitDuration() * 3;
92         mPanicThresholdMs = context.getResources()
93                 .getInteger(R.integer.config_immersive_mode_confirmation_panic);
94         mVrModeEnabled = vrModeEnabled;
95     }
96 
getNavBarExitDuration()97     private long getNavBarExitDuration() {
98         Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit);
99         return exit != null ? exit.getDuration() : 0;
100     }
101 
loadSetting(int currentUserId, Context context)102     static boolean loadSetting(int currentUserId, Context context) {
103         final boolean wasConfirmed = sConfirmed;
104         sConfirmed = false;
105         if (DEBUG) Slog.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
106         String value = null;
107         try {
108             value = Settings.Secure.getStringForUser(context.getContentResolver(),
109                     Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
110                     UserHandle.USER_CURRENT);
111             sConfirmed = CONFIRMED.equals(value);
112             if (DEBUG) Slog.d(TAG, "Loaded sConfirmed=" + sConfirmed);
113         } catch (Throwable t) {
114             Slog.w(TAG, "Error loading confirmations, value=" + value, t);
115         }
116         return sConfirmed != wasConfirmed;
117     }
118 
saveSetting(Context context)119     private static void saveSetting(Context context) {
120         if (DEBUG) Slog.d(TAG, "saveSetting()");
121         try {
122             final String value = sConfirmed ? CONFIRMED : null;
123             Settings.Secure.putStringForUser(context.getContentResolver(),
124                     Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
125                     value,
126                     UserHandle.USER_CURRENT);
127             if (DEBUG) Slog.d(TAG, "Saved value=" + value);
128         } catch (Throwable t) {
129             Slog.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
130         }
131     }
132 
immersiveModeChangedLw(String pkg, boolean isImmersiveMode, boolean userSetupComplete, boolean navBarEmpty)133     void immersiveModeChangedLw(String pkg, boolean isImmersiveMode,
134             boolean userSetupComplete, boolean navBarEmpty) {
135         mHandler.removeMessages(H.SHOW);
136         if (isImmersiveMode) {
137             final boolean disabled = PolicyControl.disableImmersiveConfirmation(pkg);
138             if (DEBUG) Slog.d(TAG, String.format("immersiveModeChanged() disabled=%s sConfirmed=%s",
139                     disabled, sConfirmed));
140             if (!disabled
141                     && (DEBUG_SHOW_EVERY_TIME || !sConfirmed)
142                     && userSetupComplete
143                     && !mVrModeEnabled
144                     && !navBarEmpty
145                     && !UserManager.isDeviceInDemoMode(mContext)
146                     && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
147                 mHandler.sendEmptyMessageDelayed(H.SHOW, mShowDelayMs);
148             }
149         } else {
150             mHandler.sendEmptyMessage(H.HIDE);
151         }
152     }
153 
onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode, boolean navBarEmpty)154     boolean onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode,
155             boolean navBarEmpty) {
156         if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) {
157             // turning the screen back on within the panic threshold
158             return mClingWindow == null;
159         }
160         if (isScreenOn && inImmersiveMode && !navBarEmpty) {
161             // turning the screen off, remember if we were in immersive mode
162             mPanicTime = time;
163         } else {
164             mPanicTime = 0;
165         }
166         return false;
167     }
168 
confirmCurrentPrompt()169     void confirmCurrentPrompt() {
170         if (mClingWindow != null) {
171             if (DEBUG) Slog.d(TAG, "confirmCurrentPrompt()");
172             mHandler.post(mConfirm);
173         }
174     }
175 
handleHide()176     private void handleHide() {
177         if (mClingWindow != null) {
178             if (DEBUG) Slog.d(TAG, "Hiding immersive mode confirmation");
179             getWindowManager().removeView(mClingWindow);
180             mClingWindow = null;
181         }
182     }
183 
getClingWindowLayoutParams()184     private WindowManager.LayoutParams getClingWindowLayoutParams() {
185         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
186                 ViewGroup.LayoutParams.MATCH_PARENT,
187                 ViewGroup.LayoutParams.MATCH_PARENT,
188                 WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL,
189                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
190                         | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
191                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
192                 PixelFormat.TRANSLUCENT);
193         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
194         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
195         lp.setTitle("ImmersiveModeConfirmation");
196         lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
197         lp.token = getWindowToken();
198         return lp;
199     }
200 
getBubbleLayoutParams()201     private FrameLayout.LayoutParams getBubbleLayoutParams() {
202         return new FrameLayout.LayoutParams(
203                 mContext.getResources().getDimensionPixelSize(
204                         R.dimen.immersive_mode_cling_width),
205                 ViewGroup.LayoutParams.WRAP_CONTENT,
206                 Gravity.CENTER_HORIZONTAL | Gravity.TOP);
207     }
208 
209     /**
210      * @return the window token that's used by all ImmersiveModeConfirmation windows.
211      */
getWindowToken()212     IBinder getWindowToken() {
213         return mWindowToken;
214     }
215 
216     private class ClingWindowView extends FrameLayout {
217         private static final int BGCOLOR = 0x80000000;
218         private static final int OFFSET_DP = 96;
219         private static final int ANIMATION_DURATION = 250;
220 
221         private final Runnable mConfirm;
222         private final ColorDrawable mColor = new ColorDrawable(0);
223         private final Interpolator mInterpolator;
224         private ValueAnimator mColorAnim;
225         private ViewGroup mClingLayout;
226 
227         private Runnable mUpdateLayoutRunnable = new Runnable() {
228             @Override
229             public void run() {
230                 if (mClingLayout != null && mClingLayout.getParent() != null) {
231                     mClingLayout.setLayoutParams(getBubbleLayoutParams());
232                 }
233             }
234         };
235 
236         private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
237                 new ViewTreeObserver.OnComputeInternalInsetsListener() {
238                     private final int[] mTmpInt2 = new int[2];
239 
240                     @Override
241                     public void onComputeInternalInsets(
242                             ViewTreeObserver.InternalInsetsInfo inoutInfo) {
243                         // Set touchable region to cover the cling layout.
244                         mClingLayout.getLocationInWindow(mTmpInt2);
245                         inoutInfo.setTouchableInsets(
246                                 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
247                         inoutInfo.touchableRegion.set(
248                                 mTmpInt2[0],
249                                 mTmpInt2[1],
250                                 mTmpInt2[0] + mClingLayout.getWidth(),
251                                 mTmpInt2[1] + mClingLayout.getHeight());
252                     }
253                 };
254 
255         private BroadcastReceiver mReceiver = new BroadcastReceiver() {
256             @Override
257             public void onReceive(Context context, Intent intent) {
258                 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
259                     post(mUpdateLayoutRunnable);
260                 }
261             }
262         };
263 
ClingWindowView(Context context, Runnable confirm)264         ClingWindowView(Context context, Runnable confirm) {
265             super(context);
266             mConfirm = confirm;
267             setBackground(mColor);
268             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
269             mInterpolator = AnimationUtils
270                     .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
271         }
272 
273         @Override
onAttachedToWindow()274         public void onAttachedToWindow() {
275             super.onAttachedToWindow();
276 
277             DisplayMetrics metrics = new DisplayMetrics();
278             mContext.getDisplay().getMetrics(metrics);
279             float density = metrics.density;
280 
281             getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
282 
283             // create the confirmation cling
284             mClingLayout = (ViewGroup)
285                     View.inflate(getContext(), R.layout.immersive_mode_cling, null);
286 
287             final Button ok = mClingLayout.findViewById(R.id.ok);
288             ok.setOnClickListener(new OnClickListener() {
289                 @Override
290                 public void onClick(View v) {
291                     mConfirm.run();
292                 }
293             });
294             addView(mClingLayout, getBubbleLayoutParams());
295 
296             if (ActivityManager.isHighEndGfx()) {
297                 final View cling = mClingLayout;
298                 cling.setAlpha(0f);
299                 cling.setTranslationY(-OFFSET_DP * density);
300 
301                 postOnAnimation(new Runnable() {
302                     @Override
303                     public void run() {
304                         cling.animate()
305                                 .alpha(1f)
306                                 .translationY(0)
307                                 .setDuration(ANIMATION_DURATION)
308                                 .setInterpolator(mInterpolator)
309                                 .withLayer()
310                                 .start();
311 
312                         mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
313                         mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
314                             @Override
315                             public void onAnimationUpdate(ValueAnimator animation) {
316                                 final int c = (Integer) animation.getAnimatedValue();
317                                 mColor.setColor(c);
318                             }
319                         });
320                         mColorAnim.setDuration(ANIMATION_DURATION);
321                         mColorAnim.setInterpolator(mInterpolator);
322                         mColorAnim.start();
323                     }
324                 });
325             } else {
326                 mColor.setColor(BGCOLOR);
327             }
328 
329             mContext.registerReceiver(mReceiver,
330                     new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
331         }
332 
333         @Override
onDetachedFromWindow()334         public void onDetachedFromWindow() {
335             mContext.unregisterReceiver(mReceiver);
336         }
337 
338         @Override
onTouchEvent(MotionEvent motion)339         public boolean onTouchEvent(MotionEvent motion) {
340             return true;
341         }
342     }
343 
344     /**
345      * DO HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD
346      * The reason why we add this method is to avoid the deadlock of WMG->WMS and WMS->WMG
347      * when ImmersiveModeConfirmation object is created.
348      */
getWindowManager()349     private WindowManager getWindowManager() {
350         if (mWindowManager == null) {
351             mWindowManager = (WindowManager)
352                       mContext.getSystemService(Context.WINDOW_SERVICE);
353         }
354         return mWindowManager;
355     }
356 
handleShow()357     private void handleShow() {
358         if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation");
359 
360         mClingWindow = new ClingWindowView(mContext, mConfirm);
361 
362         // we will be hiding the nav bar, so layout as if it's already hidden
363         mClingWindow.setSystemUiVisibility(
364                 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
365 
366         // show the confirmation
367         WindowManager.LayoutParams lp = getClingWindowLayoutParams();
368         getWindowManager().addView(mClingWindow, lp);
369     }
370 
371     private final Runnable mConfirm = new Runnable() {
372         @Override
373         public void run() {
374             if (DEBUG) Slog.d(TAG, "mConfirm.run()");
375             if (!sConfirmed) {
376                 sConfirmed = true;
377                 saveSetting(mContext);
378             }
379             handleHide();
380         }
381     };
382 
383     private final class H extends Handler {
384         private static final int SHOW = 1;
385         private static final int HIDE = 2;
386 
H(Looper looper)387         H(Looper looper) {
388             super(looper);
389         }
390 
391         @Override
handleMessage(Message msg)392         public void handleMessage(Message msg) {
393             switch(msg.what) {
394                 case SHOW:
395                     handleShow();
396                     break;
397                 case HIDE:
398                     handleHide();
399                     break;
400             }
401         }
402     }
403 
onVrStateChangedLw(boolean enabled)404     void onVrStateChangedLw(boolean enabled) {
405         mVrModeEnabled = enabled;
406         if (mVrModeEnabled) {
407             mHandler.removeMessages(H.SHOW);
408             mHandler.sendEmptyMessage(H.HIDE);
409         }
410     }
411 
onLockTaskModeChangedLw(int lockTaskState)412     void onLockTaskModeChangedLw(int lockTaskState) {
413         mLockTaskState = lockTaskState;
414     }
415 }
416