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