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