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