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