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