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.server.policy;
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.RemoteException;
31 import android.os.ServiceManager;
32 import android.os.UserHandle;
33 import android.provider.Settings;
34 import android.service.vr.IVrManager;
35 import android.service.vr.IVrStateCallbacks;
36 import android.util.DisplayMetrics;
37 import android.util.Slog;
38 import android.util.SparseBooleanArray;
39 import android.view.Gravity;
40 import android.view.MotionEvent;
41 import android.view.View;
42 import android.view.ViewGroup;
43 import android.view.ViewTreeObserver;
44 import android.view.WindowManager;
45 import android.view.animation.Animation;
46 import android.view.animation.AnimationUtils;
47 import android.view.animation.Interpolator;
48 import android.widget.Button;
49 import android.widget.FrameLayout;
50 
51 import com.android.internal.R;
52 import 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  */
58 public 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 
ImmersiveModeConfirmation(Context context)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 
getNavBarExitDuration()88     private long getNavBarExitDuration() {
89         Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit);
90         return exit != null ? exit.getDuration() : 0;
91     }
92 
loadSetting(int currentUserId)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 
saveSetting()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 
systemReady()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 
immersiveModeChangedLw(String pkg, boolean isImmersiveMode, boolean userSetupComplete)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 
onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode)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 
confirmCurrentPrompt()167     public void confirmCurrentPrompt() {
168         if (mClingWindow != null) {
169             if (DEBUG) Slog.d(TAG, "confirmCurrentPrompt()");
170             mHandler.post(mConfirm);
171         }
172     }
173 
handleHide()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 
getClingWindowLayoutParams()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 
getBubbleLayoutParams()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 
ClingWindowView(Context context, Runnable confirm)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
onAttachedToWindow()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
onDetachedFromWindow()325         public void onDetachedFromWindow() {
326             mContext.unregisterReceiver(mReceiver);
327         }
328 
329         @Override
onTouchEvent(MotionEvent motion)330         public boolean onTouchEvent(MotionEvent motion) {
331             return true;
332         }
333     }
334 
handleShow()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
handleMessage(Message msg)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             if (mVrModeEnabled) {
384                 mHandler.removeMessages(H.SHOW);
385                 mHandler.sendEmptyMessage(H.HIDE);
386             }
387         }
388     };
389 }
390