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