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