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