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