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 int mLockTaskState = LOCK_TASK_MODE_NONE; 103 ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled)104 ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled) { 105 final Display display = context.getDisplay(); 106 final Context uiContext = ActivityThread.currentActivityThread().getSystemUiContext(); 107 mContext = display.getDisplayId() == DEFAULT_DISPLAY 108 ? uiContext : uiContext.createDisplayContext(display); 109 mHandler = new H(looper); 110 mShowDelayMs = getNavBarExitDuration() * 3; 111 mPanicThresholdMs = context.getResources() 112 .getInteger(R.integer.config_immersive_mode_confirmation_panic); 113 mVrModeEnabled = vrModeEnabled; 114 } 115 getNavBarExitDuration()116 private long getNavBarExitDuration() { 117 Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit); 118 return exit != null ? exit.getDuration() : 0; 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 onSettingChanged(int currentUserId)152 boolean onSettingChanged(int currentUserId) { 153 final boolean changed = loadSetting(currentUserId, mContext); 154 // Remove the window if the setting changes to be confirmed. 155 if (changed && sConfirmed) { 156 mHandler.sendEmptyMessage(H.HIDE); 157 } 158 return changed; 159 } 160 immersiveModeChangedLw(int rootDisplayAreaId, boolean isImmersiveMode, boolean userSetupComplete, boolean navBarEmpty)161 void immersiveModeChangedLw(int rootDisplayAreaId, boolean isImmersiveMode, 162 boolean userSetupComplete, boolean navBarEmpty) { 163 mHandler.removeMessages(H.SHOW); 164 if (isImmersiveMode) { 165 if (DEBUG) Slog.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed); 166 if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed) 167 && userSetupComplete 168 && !mVrModeEnabled 169 && !navBarEmpty 170 && !UserManager.isDeviceInDemoMode(mContext) 171 && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) { 172 final Message msg = mHandler.obtainMessage(H.SHOW); 173 msg.arg1 = rootDisplayAreaId; 174 mHandler.sendMessageDelayed(msg, mShowDelayMs); 175 } 176 } else { 177 mHandler.sendEmptyMessage(H.HIDE); 178 } 179 } 180 onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode, boolean navBarEmpty)181 boolean onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode, 182 boolean navBarEmpty) { 183 if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) { 184 // turning the screen back on within the panic threshold 185 return mClingWindow == null; 186 } 187 if (isScreenOn && inImmersiveMode && !navBarEmpty) { 188 // turning the screen off, remember if we were in immersive mode 189 mPanicTime = time; 190 } else { 191 mPanicTime = 0; 192 } 193 return false; 194 } 195 confirmCurrentPrompt()196 void confirmCurrentPrompt() { 197 if (mClingWindow != null) { 198 if (DEBUG) Slog.d(TAG, "confirmCurrentPrompt()"); 199 mHandler.post(mConfirm); 200 } 201 } 202 handleHide()203 private void handleHide() { 204 if (mClingWindow != null) { 205 if (DEBUG) Slog.d(TAG, "Hiding immersive mode confirmation"); 206 // We don't care which root display area the window manager is specifying for removal. 207 getWindowManager(FEATURE_UNDEFINED).removeView(mClingWindow); 208 mClingWindow = null; 209 } 210 } 211 getClingWindowLayoutParams()212 private WindowManager.LayoutParams getClingWindowLayoutParams() { 213 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 214 ViewGroup.LayoutParams.MATCH_PARENT, 215 ViewGroup.LayoutParams.MATCH_PARENT, 216 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, 217 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 218 | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED 219 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, 220 PixelFormat.TRANSLUCENT); 221 lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars()); 222 // Trusted overlay so touches outside the touchable area are allowed to pass through 223 lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS 224 | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 225 lp.setTitle("ImmersiveModeConfirmation"); 226 lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation; 227 lp.token = getWindowToken(); 228 return lp; 229 } 230 getBubbleLayoutParams()231 private FrameLayout.LayoutParams getBubbleLayoutParams() { 232 return new FrameLayout.LayoutParams( 233 mContext.getResources().getDimensionPixelSize( 234 R.dimen.immersive_mode_cling_width), 235 ViewGroup.LayoutParams.WRAP_CONTENT, 236 Gravity.CENTER_HORIZONTAL | Gravity.TOP); 237 } 238 239 /** 240 * @return the window token that's used by all ImmersiveModeConfirmation windows. 241 */ getWindowToken()242 IBinder getWindowToken() { 243 return mWindowToken; 244 } 245 246 private class ClingWindowView extends FrameLayout { 247 private static final int BGCOLOR = 0x80000000; 248 private static final int OFFSET_DP = 96; 249 private static final int ANIMATION_DURATION = 250; 250 251 private final Runnable mConfirm; 252 private final ColorDrawable mColor = new ColorDrawable(0); 253 private final Interpolator mInterpolator; 254 private ValueAnimator mColorAnim; 255 private ViewGroup mClingLayout; 256 257 private Runnable mUpdateLayoutRunnable = new Runnable() { 258 @Override 259 public void run() { 260 if (mClingLayout != null && mClingLayout.getParent() != null) { 261 mClingLayout.setLayoutParams(getBubbleLayoutParams()); 262 } 263 } 264 }; 265 266 private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = 267 new ViewTreeObserver.OnComputeInternalInsetsListener() { 268 private final int[] mTmpInt2 = new int[2]; 269 270 @Override 271 public void onComputeInternalInsets( 272 ViewTreeObserver.InternalInsetsInfo inoutInfo) { 273 // Set touchable region to cover the cling layout. 274 mClingLayout.getLocationInWindow(mTmpInt2); 275 inoutInfo.setTouchableInsets( 276 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 277 inoutInfo.touchableRegion.set( 278 mTmpInt2[0], 279 mTmpInt2[1], 280 mTmpInt2[0] + mClingLayout.getWidth(), 281 mTmpInt2[1] + mClingLayout.getHeight()); 282 } 283 }; 284 285 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 286 @Override 287 public void onReceive(Context context, Intent intent) { 288 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { 289 post(mUpdateLayoutRunnable); 290 } 291 } 292 }; 293 ClingWindowView(Context context, Runnable confirm)294 ClingWindowView(Context context, Runnable confirm) { 295 super(context); 296 mConfirm = confirm; 297 setBackground(mColor); 298 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); 299 mInterpolator = AnimationUtils 300 .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); 301 } 302 303 @Override onAttachedToWindow()304 public void onAttachedToWindow() { 305 super.onAttachedToWindow(); 306 307 DisplayMetrics metrics = new DisplayMetrics(); 308 mContext.getDisplay().getMetrics(metrics); 309 float density = metrics.density; 310 311 getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener); 312 313 // create the confirmation cling 314 mClingLayout = (ViewGroup) 315 View.inflate(getContext(), R.layout.immersive_mode_cling, null); 316 317 final Button ok = mClingLayout.findViewById(R.id.ok); 318 ok.setOnClickListener(new OnClickListener() { 319 @Override 320 public void onClick(View v) { 321 mConfirm.run(); 322 } 323 }); 324 addView(mClingLayout, getBubbleLayoutParams()); 325 326 if (ActivityManager.isHighEndGfx()) { 327 final View cling = mClingLayout; 328 cling.setAlpha(0f); 329 cling.setTranslationY(-OFFSET_DP * density); 330 331 postOnAnimation(new Runnable() { 332 @Override 333 public void run() { 334 cling.animate() 335 .alpha(1f) 336 .translationY(0) 337 .setDuration(ANIMATION_DURATION) 338 .setInterpolator(mInterpolator) 339 .withLayer() 340 .start(); 341 342 mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR); 343 mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 344 @Override 345 public void onAnimationUpdate(ValueAnimator animation) { 346 final int c = (Integer) animation.getAnimatedValue(); 347 mColor.setColor(c); 348 } 349 }); 350 mColorAnim.setDuration(ANIMATION_DURATION); 351 mColorAnim.setInterpolator(mInterpolator); 352 mColorAnim.start(); 353 } 354 }); 355 } else { 356 mColor.setColor(BGCOLOR); 357 } 358 359 mContext.registerReceiver(mReceiver, 360 new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); 361 } 362 363 @Override onDetachedFromWindow()364 public void onDetachedFromWindow() { 365 mContext.unregisterReceiver(mReceiver); 366 } 367 368 @Override onTouchEvent(MotionEvent motion)369 public boolean onTouchEvent(MotionEvent motion) { 370 return true; 371 } 372 373 @Override onApplyWindowInsets(WindowInsets insets)374 public WindowInsets onApplyWindowInsets(WindowInsets insets) { 375 // we will be hiding the nav bar, so layout as if it's already hidden 376 return new WindowInsets.Builder(insets).setInsets( 377 Type.systemBars(), Insets.NONE).build(); 378 } 379 } 380 381 /** 382 * DO HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD 383 * The reason why we add this method is to avoid the deadlock of WMG->WMS and WMS->WMG 384 * when ImmersiveModeConfirmation object is created. 385 * 386 * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the 387 * confirmation window. 388 */ getWindowManager(int rootDisplayAreaId)389 private WindowManager getWindowManager(int rootDisplayAreaId) { 390 if (mWindowManager == null || mWindowContext == null) { 391 // Create window context to specify the RootDisplayArea 392 final Bundle options = getOptionsForWindowContext(rootDisplayAreaId); 393 mWindowContext = mContext.createWindowContext( 394 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options); 395 mWindowManager = mWindowContext.getSystemService(WindowManager.class); 396 return mWindowManager; 397 } 398 399 // Update the window context and window manager to specify the RootDisplayArea 400 final Bundle options = getOptionsForWindowContext(rootDisplayAreaId); 401 final IWindowManager wms = WindowManagerGlobal.getWindowManagerService(); 402 try { 403 wms.attachWindowContextToDisplayArea(mWindowContext.getWindowContextToken(), 404 IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, mContext.getDisplayId(), options); 405 } catch (RemoteException e) { 406 throw e.rethrowAsRuntimeException(); 407 } 408 409 return mWindowManager; 410 } 411 412 /** 413 * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window. 414 * {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}. 415 */ 416 @Nullable getOptionsForWindowContext(int rootDisplayAreaId)417 private Bundle getOptionsForWindowContext(int rootDisplayAreaId) { 418 // In case we don't care which root display area the window manager is specifying. 419 if (rootDisplayAreaId == FEATURE_UNDEFINED) { 420 return null; 421 } 422 423 final Bundle options = new Bundle(); 424 options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId); 425 return options; 426 } 427 handleShow(int rootDisplayAreaId)428 private void handleShow(int rootDisplayAreaId) { 429 if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation"); 430 431 mClingWindow = new ClingWindowView(mContext, mConfirm); 432 433 // show the confirmation 434 WindowManager.LayoutParams lp = getClingWindowLayoutParams(); 435 getWindowManager(rootDisplayAreaId).addView(mClingWindow, lp); 436 } 437 438 private final Runnable mConfirm = new Runnable() { 439 @Override 440 public void run() { 441 if (DEBUG) Slog.d(TAG, "mConfirm.run()"); 442 if (!sConfirmed) { 443 sConfirmed = true; 444 saveSetting(mContext); 445 } 446 handleHide(); 447 } 448 }; 449 450 private final class H extends Handler { 451 private static final int SHOW = 1; 452 private static final int HIDE = 2; 453 H(Looper looper)454 H(Looper looper) { 455 super(looper); 456 } 457 458 @Override handleMessage(Message msg)459 public void handleMessage(Message msg) { 460 switch(msg.what) { 461 case SHOW: 462 handleShow(msg.arg1); 463 break; 464 case HIDE: 465 handleHide(); 466 break; 467 } 468 } 469 } 470 onVrStateChangedLw(boolean enabled)471 void onVrStateChangedLw(boolean enabled) { 472 mVrModeEnabled = enabled; 473 if (mVrModeEnabled) { 474 mHandler.removeMessages(H.SHOW); 475 mHandler.sendEmptyMessage(H.HIDE); 476 } 477 } 478 onLockTaskModeChangedLw(int lockTaskState)479 void onLockTaskModeChangedLw(int lockTaskState) { 480 mLockTaskState = lockTaskState; 481 } 482 } 483