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