1 /*
2  * Copyright (C) 2023 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.systemui.statusbar;
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.app.StatusBarManager.DISABLE_BACK;
22 import static android.app.StatusBarManager.DISABLE_HOME;
23 import static android.app.StatusBarManager.DISABLE_RECENT;
24 import static android.view.Display.DEFAULT_DISPLAY;
25 import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION;
26 import static android.view.ViewRootImpl.CLIENT_TRANSIENT;
27 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
28 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED;
29 import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID;
30 
31 import android.animation.ArgbEvaluator;
32 import android.animation.ValueAnimator;
33 import android.annotation.NonNull;
34 import android.annotation.Nullable;
35 import android.app.ActivityManager;
36 import android.content.BroadcastReceiver;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.IntentFilter;
40 import android.content.res.Resources;
41 import android.database.ContentObserver;
42 import android.graphics.Insets;
43 import android.graphics.PixelFormat;
44 import android.graphics.Rect;
45 import android.graphics.drawable.ColorDrawable;
46 import android.os.Binder;
47 import android.os.Bundle;
48 import android.os.Handler;
49 import android.os.IBinder;
50 import android.os.Looper;
51 import android.os.Message;
52 import android.os.RemoteException;
53 import android.os.ServiceManager;
54 import android.os.UserHandle;
55 import android.os.UserManager;
56 import android.provider.Settings;
57 import android.service.vr.IVrManager;
58 import android.service.vr.IVrStateCallbacks;
59 import android.util.DisplayMetrics;
60 import android.util.Log;
61 import android.view.Display;
62 import android.view.Gravity;
63 import android.view.MotionEvent;
64 import android.view.View;
65 import android.view.ViewGroup;
66 import android.view.ViewTreeObserver;
67 import android.view.WindowInsets;
68 import android.view.WindowInsets.Type;
69 import android.view.WindowManager;
70 import android.view.animation.AnimationUtils;
71 import android.view.animation.Interpolator;
72 import android.widget.Button;
73 import android.widget.FrameLayout;
74 import android.widget.RelativeLayout;
75 
76 import com.android.systemui.CoreStartable;
77 import com.android.systemui.res.R;
78 import com.android.systemui.shared.system.TaskStackChangeListener;
79 import com.android.systemui.shared.system.TaskStackChangeListeners;
80 import com.android.systemui.util.settings.SecureSettings;
81 
82 import javax.inject.Inject;
83 
84 /**
85  *  Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden
86  *  entering immersive mode.
87  */
88 public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Callbacks,
89         TaskStackChangeListener {
90     private static final String TAG = "ImmersiveModeConfirm";
91     private static final boolean DEBUG = false;
92     private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution
93     private static final String CONFIRMED = "confirmed";
94     private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE =
95             WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
96 
97     private static boolean sConfirmed;
98     private final SecureSettings mSecureSettings;
99 
100     private Context mDisplayContext;
101     private final Context mSysUiContext;
102     private final Handler mHandler = new H(Looper.getMainLooper());
103     private long mShowDelayMs = 0L;
104     private final IBinder mWindowToken = new Binder();
105     private final CommandQueue mCommandQueue;
106 
107     private ClingWindowView mClingWindow;
108     /** The last {@link WindowManager} that is used to add the confirmation window. */
109     @Nullable
110     private WindowManager mWindowManager;
111     /**
112      * The WindowContext that is registered with {@link #mWindowManager} with options to specify the
113      * {@link RootDisplayArea} to attach the confirmation window.
114      */
115     @Nullable
116     private Context mWindowContext;
117     /**
118      * The root display area feature id that the {@link #mWindowContext} is attaching to.
119      */
120     private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED;
121     // Local copy of vr mode enabled state, to avoid calling into VrManager with
122     // the lock held.
123     private boolean mVrModeEnabled = false;
124     private boolean mCanSystemBarsBeShownByUser = true;
125     private int mLockTaskState = LOCK_TASK_MODE_NONE;
126     private boolean mNavBarEmpty;
127 
128     private ContentObserver mContentObserver;
129 
130     @Inject
ImmersiveModeConfirmation(Context context, CommandQueue commandQueue, SecureSettings secureSettings)131     public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue,
132                                      SecureSettings secureSettings) {
133         mSysUiContext = context;
134         final Display display = mSysUiContext.getDisplay();
135         mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY
136                 ? mSysUiContext : mSysUiContext.createDisplayContext(display);
137         mCommandQueue = commandQueue;
138         mSecureSettings = secureSettings;
139     }
140 
loadSetting(int currentUserId)141     boolean loadSetting(int currentUserId) {
142         final boolean wasConfirmed = sConfirmed;
143         sConfirmed = false;
144         if (DEBUG) Log.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId));
145         String value = null;
146         try {
147             value = mSecureSettings.getStringForUser(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
148                     UserHandle.USER_CURRENT);
149             sConfirmed = CONFIRMED.equals(value);
150             if (DEBUG) Log.d(TAG, "Loaded sConfirmed=" + sConfirmed);
151         } catch (Throwable t) {
152             Log.w(TAG, "Error loading confirmations, value=" + value, t);
153         }
154         return sConfirmed != wasConfirmed;
155     }
156 
saveSetting(Context context)157     private static void saveSetting(Context context) {
158         if (DEBUG) Log.d(TAG, "saveSetting()");
159         try {
160             final String value = sConfirmed ? CONFIRMED : null;
161             Settings.Secure.putStringForUser(context.getContentResolver(),
162                     Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS,
163                     value,
164                     UserHandle.USER_CURRENT);
165             if (DEBUG) Log.d(TAG, "Saved value=" + value);
166         } catch (Throwable t) {
167             Log.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t);
168         }
169     }
170 
171     @Override
onDisplayRemoved(int displayId)172     public void onDisplayRemoved(int displayId) {
173         if (displayId != mSysUiContext.getDisplayId()) {
174             return;
175         }
176         mHandler.removeMessages(H.SHOW);
177         mHandler.removeMessages(H.HIDE);
178         IVrManager vrManager = IVrManager.Stub.asInterface(
179                 ServiceManager.getService(Context.VR_SERVICE));
180         if (vrManager != null) {
181             try {
182                 vrManager.unregisterListener(mVrStateCallbacks);
183             } catch (RemoteException ex) {
184             }
185         }
186         mCommandQueue.removeCallback(this);
187     }
188 
onSettingChanged(int currentUserId)189     private void onSettingChanged(int currentUserId) {
190         final boolean changed = loadSetting(currentUserId);
191         // Remove the window if the setting changes to be confirmed.
192         if (changed && sConfirmed) {
193             mHandler.sendEmptyMessage(H.HIDE);
194         }
195     }
196 
197     @Override
immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode)198     public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {
199         mHandler.removeMessages(H.SHOW);
200         if (isImmersiveMode) {
201             if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" +  sConfirmed);
202             boolean userSetupComplete = (mSecureSettings.getIntForUser(
203                     Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0);
204 
205             if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed)
206                     && userSetupComplete
207                     && !mVrModeEnabled
208                     && mCanSystemBarsBeShownByUser
209                     && !mNavBarEmpty
210                     && !UserManager.isDeviceInDemoMode(mDisplayContext)
211                     && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) {
212                 final Message msg = mHandler.obtainMessage(
213                         H.SHOW);
214                 msg.arg1 = rootDisplayAreaId;
215                 mHandler.sendMessageDelayed(msg, mShowDelayMs);
216             }
217         } else {
218             mHandler.sendEmptyMessage(H.HIDE);
219         }
220     }
221 
222     @Override
disable(int displayId, int disableFlag, int disableFlag2, boolean animate)223     public void disable(int displayId, int disableFlag, int disableFlag2, boolean animate) {
224         if (mSysUiContext.getDisplayId() != displayId) {
225             return;
226         }
227         final int disableNavigationBar = (DISABLE_HOME | DISABLE_BACK | DISABLE_RECENT);
228         mNavBarEmpty = (disableFlag & disableNavigationBar) == disableNavigationBar;
229     }
230 
231     @Override
confirmImmersivePrompt()232     public void confirmImmersivePrompt() {
233         if (mClingWindow != null) {
234             if (DEBUG) Log.d(TAG, "confirmImmersivePrompt()");
235             mHandler.post(mConfirm);
236         }
237     }
238 
handleHide()239     private void handleHide() {
240         if (mClingWindow != null) {
241             if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation");
242             if (mWindowManager != null) {
243                 try {
244                     mWindowManager.removeView(mClingWindow);
245                 } catch (WindowManager.InvalidDisplayException e) {
246                     Log.w(TAG, "Fail to hide the immersive confirmation window because of "
247                             + e);
248                 }
249                 mWindowManager = null;
250                 mWindowContext = null;
251             }
252             mClingWindow = null;
253         }
254     }
255 
getClingWindowLayoutParams()256     private WindowManager.LayoutParams getClingWindowLayoutParams() {
257         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
258                 ViewGroup.LayoutParams.MATCH_PARENT,
259                 ViewGroup.LayoutParams.MATCH_PARENT,
260                 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE,
261                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
262                         | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
263                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
264                 PixelFormat.TRANSLUCENT);
265         lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars());
266         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
267         // Trusted overlay so touches outside the touchable area are allowed to pass through
268         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
269                 | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
270                 | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW;
271         lp.setTitle("ImmersiveModeConfirmation");
272         lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation;
273         lp.token = getWindowToken();
274         return lp;
275     }
276 
getBubbleLayoutParams()277     private FrameLayout.LayoutParams getBubbleLayoutParams() {
278         return new FrameLayout.LayoutParams(
279                 getClingWindowWidth(),
280                 ViewGroup.LayoutParams.WRAP_CONTENT,
281                 Gravity.CENTER_HORIZONTAL | Gravity.TOP);
282     }
283 
284     /**
285      * Returns the width of the cling window.
286      */
getClingWindowWidth()287     private int getClingWindowWidth() {
288         return mSysUiContext.getResources().getDimensionPixelSize(
289                 R.dimen.immersive_mode_cling_width);
290     }
291 
292     /**
293      * @return the window token that's used by all ImmersiveModeConfirmation windows.
294      */
getWindowToken()295     IBinder getWindowToken() {
296         return mWindowToken;
297     }
298 
299     @Override
start()300     public void start() {
301         if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) {
302             mCommandQueue.addCallback(this);
303 
304             final Resources r = mSysUiContext.getResources();
305             mShowDelayMs = r.getInteger(R.integer.dock_enter_exit_duration) * 3L;
306             mCanSystemBarsBeShownByUser = !r.getBoolean(
307                     R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean(
308                     R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction);
309             IVrManager vrManager = IVrManager.Stub.asInterface(
310                     ServiceManager.getService(Context.VR_SERVICE));
311             if (vrManager != null) {
312                 try {
313                     mVrModeEnabled = vrManager.getVrModeState();
314                     vrManager.registerListener(mVrStateCallbacks);
315                     mVrStateCallbacks.onVrStateChanged(mVrModeEnabled);
316                 } catch (RemoteException e) {
317                     // Ignore, we cannot do anything if we failed to access vr manager.
318                 }
319             }
320             TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
321             mContentObserver = new ContentObserver(mHandler) {
322                 @Override
323                 public void onChange(boolean selfChange) {
324                     onSettingChanged(mSysUiContext.getUserId());
325                 }
326             };
327 
328             // Register to listen for changes in Settings.Secure settings.
329             mSecureSettings.registerContentObserverForUserSync(
330                     Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, mContentObserver,
331                     UserHandle.USER_CURRENT);
332             mSecureSettings.registerContentObserverForUserSync(
333                     Settings.Secure.USER_SETUP_COMPLETE, mContentObserver,
334                     UserHandle.USER_CURRENT);
335         }
336     }
337 
338     private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() {
339         @Override
340         public void onVrStateChanged(boolean enabled) {
341             mVrModeEnabled = enabled;
342             if (mVrModeEnabled) {
343                 mHandler.removeMessages(H.SHOW);
344                 mHandler.sendEmptyMessage(H.HIDE);
345             }
346         }
347     };
348 
349     private class ClingWindowView extends FrameLayout {
350         private static final int BGCOLOR = 0x80000000;
351         private static final int OFFSET_DP = 96;
352         private static final int ANIMATION_DURATION = 250;
353 
354         private final Runnable mConfirm;
355         private final ColorDrawable mColor = new ColorDrawable(0);
356         private final Interpolator mInterpolator;
357         private ValueAnimator mColorAnim;
358         private ViewGroup mClingLayout;
359 
360         private Runnable mUpdateLayoutRunnable = new Runnable() {
361             @Override
362             public void run() {
363                 if (mClingLayout != null && mClingLayout.getParent() != null) {
364                     mClingLayout.setLayoutParams(getBubbleLayoutParams());
365                 }
366             }
367         };
368 
369         private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener =
370                 new ViewTreeObserver.OnComputeInternalInsetsListener() {
371                     private final int[] mTmpInt2 = new int[2];
372 
373                     @Override
374                     public void onComputeInternalInsets(
375                             ViewTreeObserver.InternalInsetsInfo inoutInfo) {
376                         // Set touchable region to cover the cling layout.
377                         mClingLayout.getLocationInWindow(mTmpInt2);
378                         inoutInfo.setTouchableInsets(
379                                 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
380                         inoutInfo.touchableRegion.set(
381                                 mTmpInt2[0],
382                                 mTmpInt2[1],
383                                 mTmpInt2[0] + mClingLayout.getWidth(),
384                                 mTmpInt2[1] + mClingLayout.getHeight());
385                     }
386                 };
387 
388         private BroadcastReceiver mReceiver = new BroadcastReceiver() {
389             @Override
390             public void onReceive(Context context, Intent intent) {
391                 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
392                     post(mUpdateLayoutRunnable);
393                 }
394             }
395         };
396 
ClingWindowView(Context context, Runnable confirm)397         ClingWindowView(Context context, Runnable confirm) {
398             super(context);
399             mConfirm = confirm;
400             setBackground(mColor);
401             setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
402             mInterpolator = AnimationUtils
403                     .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in);
404         }
405 
406         @Override
onAttachedToWindow()407         public void onAttachedToWindow() {
408             super.onAttachedToWindow();
409 
410             DisplayMetrics metrics = new DisplayMetrics();
411             mContext.getDisplay().getMetrics(metrics);
412             float density = metrics.density;
413 
414             getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener);
415 
416             // create the confirmation cling
417             mClingLayout = (ViewGroup)
418                     View.inflate(mSysUiContext, R.layout.immersive_mode_cling, null);
419 
420             final Button ok = mClingLayout.findViewById(R.id.ok);
421             ok.setOnClickListener(new OnClickListener() {
422                 @Override
423                 public void onClick(View v) {
424                     mConfirm.run();
425                 }
426             });
427             addView(mClingLayout, getBubbleLayoutParams());
428 
429             if (ActivityManager.isHighEndGfx()) {
430                 final View cling = mClingLayout;
431                 cling.setAlpha(0f);
432                 cling.setTranslationY(-OFFSET_DP * density);
433 
434                 postOnAnimation(new Runnable() {
435                     @Override
436                     public void run() {
437                         cling.animate()
438                                 .alpha(1f)
439                                 .translationY(0)
440                                 .setDuration(ANIMATION_DURATION)
441                                 .setInterpolator(mInterpolator)
442                                 .withLayer()
443                                 .start();
444 
445                         mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
446                         mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
447                             @Override
448                             public void onAnimationUpdate(ValueAnimator animation) {
449                                 final int c = (Integer) animation.getAnimatedValue();
450                                 mColor.setColor(c);
451                             }
452                         });
453                         mColorAnim.setDuration(ANIMATION_DURATION);
454                         mColorAnim.setInterpolator(mInterpolator);
455                         mColorAnim.start();
456                     }
457                 });
458             } else {
459                 mColor.setColor(BGCOLOR);
460             }
461 
462             mContext.registerReceiver(mReceiver,
463                     new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
464         }
465 
466         @Override
onDetachedFromWindow()467         public void onDetachedFromWindow() {
468             mContext.unregisterReceiver(mReceiver);
469         }
470 
471         @Override
onTouchEvent(MotionEvent motion)472         public boolean onTouchEvent(MotionEvent motion) {
473             return true;
474         }
475 
476         @Override
onApplyWindowInsets(WindowInsets insets)477         public WindowInsets onApplyWindowInsets(WindowInsets insets) {
478             // If the top display cutout overlaps with the full-width (windowWidth=-1)/centered
479             // dialog, then adjust the dialog contents by the cutout
480             final int width = getWidth();
481             final int windowWidth = getClingWindowWidth();
482             final Rect topDisplayCutout = insets.getDisplayCutout() != null
483                     ? insets.getDisplayCutout().getBoundingRectTop()
484                     : new Rect();
485             final boolean intersectsTopCutout = topDisplayCutout.intersects(
486                     width - (windowWidth / 2), 0,
487                     width + (windowWidth / 2), topDisplayCutout.bottom);
488             if (mClingWindow != null &&
489                     (windowWidth < 0 || (width > 0 && intersectsTopCutout))) {
490                 final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon);
491                 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams)
492                         iconView.getLayoutParams();
493                 lp.topMargin = topDisplayCutout.bottom;
494                 iconView.setLayoutParams(lp);
495             }
496             // we will be hiding the nav bar, so layout as if it's already hidden
497             return new WindowInsets.Builder(insets).setInsets(
498                     Type.systemBars(), Insets.NONE).build();
499         }
500     }
501 
502     /**
503      * To get window manager for the display.
504      *
505      * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the
506      *         confirmation window.
507      */
508     @NonNull
createWindowManager(int rootDisplayAreaId)509     private WindowManager createWindowManager(int rootDisplayAreaId) {
510         if (mWindowManager != null) {
511             throw new IllegalStateException(
512                     "Must not create a new WindowManager while there is an existing one");
513         }
514         // Create window context to specify the RootDisplayArea
515         final Bundle options = getOptionsForWindowContext(rootDisplayAreaId);
516         mWindowContextRootDisplayAreaId = rootDisplayAreaId;
517         mWindowContext = mDisplayContext.createWindowContext(
518                 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options);
519         mWindowManager = mWindowContext.getSystemService(WindowManager.class);
520         return mWindowManager;
521     }
522 
523     /**
524      * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window.
525      *         {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}.
526      */
527     @Nullable
getOptionsForWindowContext(int rootDisplayAreaId)528     private Bundle getOptionsForWindowContext(int rootDisplayAreaId) {
529         // In case we don't care which root display area the window manager is specifying.
530         if (rootDisplayAreaId == FEATURE_UNDEFINED) {
531             return null;
532         }
533 
534         final Bundle options = new Bundle();
535         options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId);
536         return options;
537     }
538 
handleShow(int rootDisplayAreaId)539     private void handleShow(int rootDisplayAreaId) {
540         if (mClingWindow != null) {
541             if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) {
542                 if (DEBUG) Log.d(TAG, "Immersive mode confirmation has already been shown");
543                 return;
544             } else {
545                 // Hide the existing confirmation before show a new one in the new root.
546                 if (DEBUG) Log.d(TAG, "Immersive mode confirmation was shown in a different root");
547                 handleHide();
548             }
549         }
550         if (DEBUG) Log.d(TAG, "Showing immersive mode confirmation");
551         mClingWindow = new ClingWindowView(mDisplayContext, mConfirm);
552         // show the confirmation
553         final WindowManager.LayoutParams lp = getClingWindowLayoutParams();
554         try {
555             createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp);
556         } catch (WindowManager.InvalidDisplayException e) {
557             Log.w(TAG, "Fail to show the immersive confirmation window because of " + e);
558         }
559     }
560 
561     private final Runnable mConfirm = new Runnable() {
562         @Override
563         public void run() {
564             if (DEBUG) Log.d(TAG, "mConfirm.run()");
565             if (!sConfirmed) {
566                 sConfirmed = true;
567                 saveSetting(mDisplayContext);
568             }
569             handleHide();
570         }
571     };
572 
573     private final class H extends Handler {
574         private static final int SHOW = 1;
575         private static final int HIDE = 2;
576 
H(Looper looper)577         H(Looper looper) {
578             super(looper);
579         }
580 
581         @Override
handleMessage(Message msg)582         public void handleMessage(Message msg) {
583             if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) {
584                 return;
585             }
586             switch(msg.what) {
587                 case SHOW:
588                     handleShow(msg.arg1);
589                     break;
590                 case HIDE:
591                     handleHide();
592                     break;
593             }
594         }
595     }
596 
597     @Override
onLockTaskModeChanged(int lockTaskState)598     public void onLockTaskModeChanged(int lockTaskState) {
599         mLockTaskState = lockTaskState;
600     }
601 }
602