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