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