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