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.view.ViewRootImpl.CLIENT_TRANSIENT; 23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 24 import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; 25 import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID; 26 27 import android.animation.ArgbEvaluator; 28 import android.animation.ValueAnimator; 29 import android.annotation.NonNull; 30 import android.annotation.Nullable; 31 import android.app.ActivityManager; 32 import android.app.ActivityThread; 33 import android.content.BroadcastReceiver; 34 import android.content.Context; 35 import android.content.Intent; 36 import android.content.IntentFilter; 37 import android.graphics.Insets; 38 import android.graphics.PixelFormat; 39 import android.graphics.Rect; 40 import android.graphics.drawable.ColorDrawable; 41 import android.os.Binder; 42 import android.os.Bundle; 43 import android.os.Handler; 44 import android.os.IBinder; 45 import android.os.Looper; 46 import android.os.Message; 47 import android.os.UserHandle; 48 import android.os.UserManager; 49 import android.provider.Settings; 50 import android.util.DisplayMetrics; 51 import android.util.Slog; 52 import android.view.Display; 53 import android.view.Gravity; 54 import android.view.MotionEvent; 55 import android.view.View; 56 import android.view.ViewGroup; 57 import android.view.ViewTreeObserver; 58 import android.view.WindowInsets; 59 import android.view.WindowInsets.Type; 60 import android.view.WindowManager; 61 import android.view.animation.AnimationUtils; 62 import android.view.animation.Interpolator; 63 import android.widget.Button; 64 import android.widget.FrameLayout; 65 import android.widget.RelativeLayout; 66 67 import com.android.internal.R; 68 69 /** 70 * Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden 71 * entering immersive mode. 72 */ 73 public class ImmersiveModeConfirmation { 74 private static final String TAG = "ImmersiveModeConfirmation"; 75 private static final boolean DEBUG = false; 76 private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution 77 private static final String CONFIRMED = "confirmed"; 78 private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE = 79 WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; 80 81 private static boolean sConfirmed; 82 83 private final Context mContext; 84 private final H mHandler; 85 private final long mShowDelayMs; 86 private final long mPanicThresholdMs; 87 private final IBinder mWindowToken = new Binder(); 88 89 private ClingWindowView mClingWindow; 90 private long mPanicTime; 91 /** The last {@link WindowManager} that is used to add the confirmation window. */ 92 @Nullable 93 private WindowManager mWindowManager; 94 /** 95 * The WindowContext that is registered with {@link #mWindowManager} with options to specify the 96 * {@link RootDisplayArea} to attach the confirmation window. 97 */ 98 @Nullable 99 private Context mWindowContext; 100 /** 101 * The root display area feature id that the {@link #mWindowContext} is attaching to. 102 */ 103 private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED; 104 // Local copy of vr mode enabled state, to avoid calling into VrManager with 105 // the lock held. 106 private boolean mVrModeEnabled; 107 private boolean mCanSystemBarsBeShownByUser; 108 private int mLockTaskState = LOCK_TASK_MODE_NONE; 109 ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled, boolean canSystemBarsBeShownByUser)110 ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled, 111 boolean canSystemBarsBeShownByUser) { 112 final Display display = context.getDisplay(); 113 final Context uiContext = ActivityThread.currentActivityThread().getSystemUiContext(); 114 mContext = display.getDisplayId() == DEFAULT_DISPLAY 115 ? uiContext : uiContext.createDisplayContext(display); 116 mHandler = new H(looper); 117 mShowDelayMs = context.getResources().getInteger(R.integer.dock_enter_exit_duration) * 3L; 118 mPanicThresholdMs = context.getResources() 119 .getInteger(R.integer.config_immersive_mode_confirmation_panic); 120 mVrModeEnabled = vrModeEnabled; 121 mCanSystemBarsBeShownByUser = canSystemBarsBeShownByUser; 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 if (mWindowManager != null) { 216 try { 217 mWindowManager.removeView(mClingWindow); 218 } catch (WindowManager.InvalidDisplayException e) { 219 Slog.w(TAG, "Fail to hide the immersive confirmation window because of " 220 + e); 221 } 222 mWindowManager = null; 223 mWindowContext = null; 224 } 225 mClingWindow = null; 226 } 227 } 228 getClingWindowLayoutParams()229 private WindowManager.LayoutParams getClingWindowLayoutParams() { 230 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 231 ViewGroup.LayoutParams.MATCH_PARENT, 232 ViewGroup.LayoutParams.MATCH_PARENT, 233 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, 234 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 235 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED 236 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, 237 PixelFormat.TRANSLUCENT); 238 lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars()); 239 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 240 // Trusted overlay so touches outside the touchable area are allowed to pass through 241 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS 242 | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY 243 | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW; 244 lp.setTitle("ImmersiveModeConfirmation"); 245 lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation; 246 lp.token = getWindowToken(); 247 return lp; 248 } 249 getBubbleLayoutParams()250 private FrameLayout.LayoutParams getBubbleLayoutParams() { 251 return new FrameLayout.LayoutParams( 252 getClingWindowWidth(), 253 ViewGroup.LayoutParams.WRAP_CONTENT, 254 Gravity.CENTER_HORIZONTAL | Gravity.TOP); 255 } 256 257 /** 258 * Returns the width of the cling window. 259 */ getClingWindowWidth()260 private int getClingWindowWidth() { 261 return mContext.getResources().getDimensionPixelSize( 262 R.dimen.immersive_mode_cling_width); 263 } 264 265 /** 266 * @return the window token that's used by all ImmersiveModeConfirmation windows. 267 */ getWindowToken()268 IBinder getWindowToken() { 269 return mWindowToken; 270 } 271 272 private class ClingWindowView extends FrameLayout { 273 private static final int BGCOLOR = 0x80000000; 274 private static final int OFFSET_DP = 96; 275 private static final int ANIMATION_DURATION = 250; 276 277 private final Runnable mConfirm; 278 private final ColorDrawable mColor = new ColorDrawable(0); 279 private final Interpolator mInterpolator; 280 private ValueAnimator mColorAnim; 281 private ViewGroup mClingLayout; 282 283 private Runnable mUpdateLayoutRunnable = new Runnable() { 284 @Override 285 public void run() { 286 if (mClingLayout != null && mClingLayout.getParent() != null) { 287 mClingLayout.setLayoutParams(getBubbleLayoutParams()); 288 } 289 } 290 }; 291 292 private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = 293 new ViewTreeObserver.OnComputeInternalInsetsListener() { 294 private final int[] mTmpInt2 = new int[2]; 295 296 @Override 297 public void onComputeInternalInsets( 298 ViewTreeObserver.InternalInsetsInfo inoutInfo) { 299 // Set touchable region to cover the cling layout. 300 mClingLayout.getLocationInWindow(mTmpInt2); 301 inoutInfo.setTouchableInsets( 302 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 303 inoutInfo.touchableRegion.set( 304 mTmpInt2[0], 305 mTmpInt2[1], 306 mTmpInt2[0] + mClingLayout.getWidth(), 307 mTmpInt2[1] + mClingLayout.getHeight()); 308 } 309 }; 310 311 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 312 @Override 313 public void onReceive(Context context, Intent intent) { 314 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { 315 post(mUpdateLayoutRunnable); 316 } 317 } 318 }; 319 ClingWindowView(Context context, Runnable confirm)320 ClingWindowView(Context context, Runnable confirm) { 321 super(context); 322 mConfirm = confirm; 323 setBackground(mColor); 324 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 325 mInterpolator = AnimationUtils 326 .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); 327 } 328 329 @Override onAttachedToWindow()330 public void onAttachedToWindow() { 331 super.onAttachedToWindow(); 332 333 DisplayMetrics metrics = new DisplayMetrics(); 334 mContext.getDisplay().getMetrics(metrics); 335 float density = metrics.density; 336 337 getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener); 338 339 // create the confirmation cling 340 mClingLayout = (ViewGroup) 341 View.inflate(getContext(), R.layout.immersive_mode_cling, null); 342 343 final Button ok = mClingLayout.findViewById(R.id.ok); 344 ok.setOnClickListener(new OnClickListener() { 345 @Override 346 public void onClick(View v) { 347 mConfirm.run(); 348 } 349 }); 350 addView(mClingLayout, getBubbleLayoutParams()); 351 352 if (ActivityManager.isHighEndGfx()) { 353 final View cling = mClingLayout; 354 cling.setAlpha(0f); 355 cling.setTranslationY(-OFFSET_DP * density); 356 357 postOnAnimation(new Runnable() { 358 @Override 359 public void run() { 360 cling.animate() 361 .alpha(1f) 362 .translationY(0) 363 .setDuration(ANIMATION_DURATION) 364 .setInterpolator(mInterpolator) 365 .withLayer() 366 .start(); 367 368 mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR); 369 mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 370 @Override 371 public void onAnimationUpdate(ValueAnimator animation) { 372 final int c = (Integer) animation.getAnimatedValue(); 373 mColor.setColor(c); 374 } 375 }); 376 mColorAnim.setDuration(ANIMATION_DURATION); 377 mColorAnim.setInterpolator(mInterpolator); 378 mColorAnim.start(); 379 } 380 }); 381 } else { 382 mColor.setColor(BGCOLOR); 383 } 384 385 mContext.registerReceiver(mReceiver, 386 new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); 387 } 388 389 @Override onDetachedFromWindow()390 public void onDetachedFromWindow() { 391 mContext.unregisterReceiver(mReceiver); 392 } 393 394 @Override onTouchEvent(MotionEvent motion)395 public boolean onTouchEvent(MotionEvent motion) { 396 return true; 397 } 398 399 @Override onApplyWindowInsets(WindowInsets insets)400 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 401 // If the top display cutout overlaps with the full-width (windowWidth=-1)/centered 402 // dialog, then adjust the dialog contents by the cutout 403 final int width = getWidth(); 404 final int windowWidth = getClingWindowWidth(); 405 final Rect topDisplayCutout = insets.getDisplayCutout() != null 406 ? insets.getDisplayCutout().getBoundingRectTop() 407 : new Rect(); 408 final boolean intersectsTopCutout = topDisplayCutout.intersects( 409 width - (windowWidth / 2), 0, 410 width + (windowWidth / 2), topDisplayCutout.bottom); 411 if (mClingWindow != null && 412 (windowWidth < 0 || (width > 0 && intersectsTopCutout))) { 413 final View iconView = mClingWindow.findViewById(R.id.immersive_cling_icon); 414 RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) 415 iconView.getLayoutParams(); 416 lp.topMargin = topDisplayCutout.bottom; 417 iconView.setLayoutParams(lp); 418 } 419 // we will be hiding the nav bar, so layout as if it's already hidden 420 return new WindowInsets.Builder(insets).setInsets( 421 Type.systemBars(), Insets.NONE).build(); 422 } 423 } 424 425 /** 426 * DO HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD 427 * The reason why we add this method is to avoid the deadlock of WMG->WMS and WMS->WMG 428 * when ImmersiveModeConfirmation object is created. 429 * 430 * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the 431 * confirmation window. 432 */ 433 @NonNull createWindowManager(int rootDisplayAreaId)434 private WindowManager createWindowManager(int rootDisplayAreaId) { 435 if (mWindowManager != null) { 436 throw new IllegalStateException( 437 "Must not create a new WindowManager while there is an existing one"); 438 } 439 // Create window context to specify the RootDisplayArea 440 final Bundle options = getOptionsForWindowContext(rootDisplayAreaId); 441 mWindowContextRootDisplayAreaId = rootDisplayAreaId; 442 mWindowContext = mContext.createWindowContext( 443 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options); 444 mWindowManager = mWindowContext.getSystemService(WindowManager.class); 445 return mWindowManager; 446 } 447 448 /** 449 * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window. 450 * {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}. 451 */ 452 @Nullable getOptionsForWindowContext(int rootDisplayAreaId)453 private Bundle getOptionsForWindowContext(int rootDisplayAreaId) { 454 // In case we don't care which root display area the window manager is specifying. 455 if (rootDisplayAreaId == FEATURE_UNDEFINED) { 456 return null; 457 } 458 459 final Bundle options = new Bundle(); 460 options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId); 461 return options; 462 } 463 handleShow(int rootDisplayAreaId)464 private void handleShow(int rootDisplayAreaId) { 465 if (mClingWindow != null) { 466 if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) { 467 if (DEBUG) Slog.d(TAG, "Immersive mode confirmation has already been shown"); 468 return; 469 } else { 470 // Hide the existing confirmation before show a new one in the new root. 471 if (DEBUG) Slog.d(TAG, "Immersive mode confirmation was shown in a different root"); 472 handleHide(); 473 } 474 } 475 476 if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation"); 477 mClingWindow = new ClingWindowView(mContext, mConfirm); 478 // show the confirmation 479 final WindowManager.LayoutParams lp = getClingWindowLayoutParams(); 480 try { 481 createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp); 482 } catch (WindowManager.InvalidDisplayException e) { 483 Slog.w(TAG, "Fail to show the immersive confirmation window because of " + e); 484 } 485 } 486 487 private final Runnable mConfirm = new Runnable() { 488 @Override 489 public void run() { 490 if (DEBUG) Slog.d(TAG, "mConfirm.run()"); 491 if (!sConfirmed) { 492 sConfirmed = true; 493 saveSetting(mContext); 494 } 495 handleHide(); 496 } 497 }; 498 499 private final class H extends Handler { 500 private static final int SHOW = 1; 501 private static final int HIDE = 2; 502 H(Looper looper)503 H(Looper looper) { 504 super(looper); 505 } 506 507 @Override handleMessage(Message msg)508 public void handleMessage(Message msg) { 509 if (CLIENT_TRANSIENT) { 510 return; 511 } 512 switch(msg.what) { 513 case SHOW: 514 handleShow(msg.arg1); 515 break; 516 case HIDE: 517 handleHide(); 518 break; 519 } 520 } 521 } 522 onVrStateChangedLw(boolean enabled)523 void onVrStateChangedLw(boolean enabled) { 524 mVrModeEnabled = enabled; 525 if (mVrModeEnabled) { 526 mHandler.removeMessages(H.SHOW); 527 mHandler.sendEmptyMessage(H.HIDE); 528 } 529 } 530 onLockTaskModeChangedLw(int lockTaskState)531 void onLockTaskModeChangedLw(int lockTaskState) { 532 mLockTaskState = lockTaskState; 533 } 534 } 535