Home | History | Annotate | Download | only in impl
      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.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
    182         lp.setTitle("ImmersiveModeConfirmation");
    183         lp.windowAnimations = com.android.internal.R.style.Animation_RecentApplications;
    184         lp.gravity = Gravity.FILL;
    185         return lp;
    186     }
    187 
    188     public FrameLayout.LayoutParams getBubbleLayoutParams() {
    189         return new FrameLayout.LayoutParams(
    190                 mContext.getResources().getDimensionPixelSize(
    191                         R.dimen.immersive_mode_cling_width),
    192                 ViewGroup.LayoutParams.WRAP_CONTENT,
    193                 Gravity.CENTER_HORIZONTAL | Gravity.TOP);
    194     }
    195 
    196     private class ClingWindowView extends FrameLayout {
    197         private static final int BGCOLOR = 0x80000000;
    198         private static final int OFFSET_DP = 48;
    199 
    200         private final Runnable mConfirm;
    201         private final ColorDrawable mColor = new ColorDrawable(0);
    202         private ValueAnimator mColorAnim;
    203         private ViewGroup mClingLayout;
    204 
    205         private Runnable mUpdateLayoutRunnable = new Runnable() {
    206             @Override
    207             public void run() {
    208                 if (mClingLayout != null && mClingLayout.getParent() != null) {
    209                     mClingLayout.setLayoutParams(getBubbleLayoutParams());
    210                 }
    211             }
    212         };
    213 
    214         private BroadcastReceiver mReceiver = new BroadcastReceiver() {
    215             @Override
    216             public void onReceive(Context context, Intent intent) {
    217                 if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) {
    218                     post(mUpdateLayoutRunnable);
    219                 }
    220             }
    221         };
    222 
    223         public ClingWindowView(Context context, Runnable confirm) {
    224             super(context);
    225             mConfirm = confirm;
    226             setClickable(true);
    227             setBackground(mColor);
    228         }
    229 
    230         @Override
    231         public void onAttachedToWindow() {
    232             super.onAttachedToWindow();
    233 
    234             DisplayMetrics metrics = new DisplayMetrics();
    235             mWindowManager.getDefaultDisplay().getMetrics(metrics);
    236             float density = metrics.density;
    237 
    238             // create the confirmation cling
    239             mClingLayout = (ViewGroup)
    240                     View.inflate(getContext(), R.layout.immersive_mode_cling, null);
    241 
    242             final Button ok = (Button) mClingLayout.findViewById(R.id.ok);
    243             ok.setOnClickListener(new OnClickListener() {
    244                 @Override
    245                 public void onClick(View v) {
    246                     mConfirm.run();
    247                 }
    248             });
    249             addView(mClingLayout, getBubbleLayoutParams());
    250 
    251             if (ActivityManager.isHighEndGfx()) {
    252                 final View bubble = mClingLayout.findViewById(R.id.text);
    253                 bubble.setAlpha(0f);
    254                 bubble.setTranslationY(-OFFSET_DP*density);
    255                 bubble.animate()
    256                         .alpha(1f)
    257                         .translationY(0)
    258                         .setDuration(300)
    259                         .setInterpolator(new DecelerateInterpolator())
    260                         .start();
    261 
    262                 ok.setAlpha(0f);
    263                 ok.setTranslationY(-OFFSET_DP*density);
    264                 ok.animate().alpha(1f)
    265                         .translationY(0)
    266                         .setDuration(300)
    267                         .setStartDelay(200)
    268                         .setInterpolator(new DecelerateInterpolator())
    269                         .start();
    270 
    271                 mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR);
    272                 mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    273                     @Override
    274                     public void onAnimationUpdate(ValueAnimator animation) {
    275                         final int c = (Integer) animation.getAnimatedValue();
    276                         mColor.setColor(c);
    277                     }
    278                 });
    279                 mColorAnim.setDuration(1000);
    280                 mColorAnim.start();
    281             } else {
    282                 mColor.setColor(BGCOLOR);
    283             }
    284 
    285             mContext.registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED));
    286         }
    287 
    288         @Override
    289         public void onDetachedFromWindow() {
    290             mContext.unregisterReceiver(mReceiver);
    291         }
    292 
    293         @Override
    294         public boolean onTouchEvent(MotionEvent motion) {
    295             Slog.v(TAG, "ClingWindowView.onTouchEvent");
    296             return true;
    297         }
    298     }
    299 
    300     private void handleShow(String pkg) {
    301         mPromptPackage = pkg;
    302         if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation for " + pkg);
    303 
    304         mClingWindow = new ClingWindowView(mContext, confirmAction(pkg));
    305 
    306         // we will be hiding the nav bar, so layout as if it's already hidden
    307         mClingWindow.setSystemUiVisibility(
    308                 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
    309               | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
    310 
    311         // show the confirmation
    312         WindowManager.LayoutParams lp = getClingWindowLayoutParams();
    313         mWindowManager.addView(mClingWindow, lp);
    314     }
    315 
    316     private Runnable confirmAction(final String pkg) {
    317         return new Runnable() {
    318             @Override
    319             public void run() {
    320                 if (pkg != null && !mConfirmedPackages.contains(pkg)) {
    321                     if (DEBUG) Slog.d(TAG, "Confirming immersive mode for " + pkg);
    322                     mConfirmedPackages.add(pkg);
    323                     saveSetting();
    324                 }
    325                 handleHide();
    326             }
    327         };
    328     }
    329 
    330     private final class H extends Handler {
    331         private static final int SHOW = 0;
    332         private static final int HIDE = 1;
    333 
    334         @Override
    335         public void handleMessage(Message msg) {
    336             switch(msg.what) {
    337                 case SHOW:
    338                     handleShow((String)msg.obj);
    339                     break;
    340                 case HIDE:
    341                     handleHide();
    342                     break;
    343             }
    344         }
    345     }
    346 }
    347