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