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