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