| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.wm; |
| |
| import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; |
| import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; |
| import static android.view.Display.DEFAULT_DISPLAY; |
| |
| import android.animation.ArgbEvaluator; |
| import android.animation.ValueAnimator; |
| import android.app.ActivityManager; |
| import android.app.ActivityThread; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.graphics.PixelFormat; |
| import android.graphics.drawable.ColorDrawable; |
| import android.os.Binder; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.provider.Settings; |
| import android.util.DisplayMetrics; |
| import android.util.Slog; |
| import android.view.Display; |
| import android.view.Gravity; |
| import android.view.MotionEvent; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.view.ViewTreeObserver; |
| import android.view.WindowInsets.Type; |
| import android.view.WindowManager; |
| import android.view.animation.Animation; |
| import android.view.animation.AnimationUtils; |
| import android.view.animation.Interpolator; |
| import android.widget.Button; |
| import android.widget.FrameLayout; |
| |
| import com.android.internal.R; |
| |
| /** |
| * Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden |
| * entering immersive mode. |
| */ |
| public class ImmersiveModeConfirmation { |
| private static final String TAG = "ImmersiveModeConfirmation"; |
| private static final boolean DEBUG = false; |
| private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution |
| private static final String CONFIRMED = "confirmed"; |
| |
| private static boolean sConfirmed; |
| |
| private final Context mContext; |
| private final H mHandler; |
| private final long mShowDelayMs; |
| private final long mPanicThresholdMs; |
| private final IBinder mWindowToken = new Binder(); |
| |
| private ClingWindowView mClingWindow; |
| private long mPanicTime; |
| private WindowManager mWindowManager; |
| // Local copy of vr mode enabled state, to avoid calling into VrManager with |
| // the lock held. |
| private boolean mVrModeEnabled; |
| private int mLockTaskState = LOCK_TASK_MODE_NONE; |
| |
| ImmersiveModeConfirmation(Context context, Looper looper, boolean vrModeEnabled) { |
| final Display display = context.getDisplay(); |
| final Context uiContext = ActivityThread.currentActivityThread().getSystemUiContext(); |
| mContext = display.getDisplayId() == DEFAULT_DISPLAY |
| ? uiContext : uiContext.createDisplayContext(display); |
| mHandler = new H(looper); |
| mShowDelayMs = getNavBarExitDuration() * 3; |
| mPanicThresholdMs = context.getResources() |
| .getInteger(R.integer.config_immersive_mode_confirmation_panic); |
| mVrModeEnabled = vrModeEnabled; |
| } |
| |
| private long getNavBarExitDuration() { |
| Animation exit = AnimationUtils.loadAnimation(mContext, R.anim.dock_bottom_exit); |
| return exit != null ? exit.getDuration() : 0; |
| } |
| |
| static boolean loadSetting(int currentUserId, Context context) { |
| final boolean wasConfirmed = sConfirmed; |
| sConfirmed = false; |
| if (DEBUG) Slog.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId)); |
| String value = null; |
| try { |
| value = Settings.Secure.getStringForUser(context.getContentResolver(), |
| Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, |
| UserHandle.USER_CURRENT); |
| sConfirmed = CONFIRMED.equals(value); |
| if (DEBUG) Slog.d(TAG, "Loaded sConfirmed=" + sConfirmed); |
| } catch (Throwable t) { |
| Slog.w(TAG, "Error loading confirmations, value=" + value, t); |
| } |
| return sConfirmed != wasConfirmed; |
| } |
| |
| private static void saveSetting(Context context) { |
| if (DEBUG) Slog.d(TAG, "saveSetting()"); |
| try { |
| final String value = sConfirmed ? CONFIRMED : null; |
| Settings.Secure.putStringForUser(context.getContentResolver(), |
| Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, |
| value, |
| UserHandle.USER_CURRENT); |
| if (DEBUG) Slog.d(TAG, "Saved value=" + value); |
| } catch (Throwable t) { |
| Slog.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t); |
| } |
| } |
| |
| void immersiveModeChangedLw(String pkg, boolean isImmersiveMode, |
| boolean userSetupComplete, boolean navBarEmpty) { |
| mHandler.removeMessages(H.SHOW); |
| if (isImmersiveMode) { |
| final boolean disabled = PolicyControl.disableImmersiveConfirmation(pkg); |
| if (DEBUG) Slog.d(TAG, String.format("immersiveModeChanged() disabled=%s sConfirmed=%s", |
| disabled, sConfirmed)); |
| if (!disabled |
| && (DEBUG_SHOW_EVERY_TIME || !sConfirmed) |
| && userSetupComplete |
| && !mVrModeEnabled |
| && !navBarEmpty |
| && !UserManager.isDeviceInDemoMode(mContext) |
| && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) { |
| mHandler.sendEmptyMessageDelayed(H.SHOW, mShowDelayMs); |
| } |
| } else { |
| mHandler.sendEmptyMessage(H.HIDE); |
| } |
| } |
| |
| boolean onPowerKeyDown(boolean isScreenOn, long time, boolean inImmersiveMode, |
| boolean navBarEmpty) { |
| if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) { |
| // turning the screen back on within the panic threshold |
| return mClingWindow == null; |
| } |
| if (isScreenOn && inImmersiveMode && !navBarEmpty) { |
| // turning the screen off, remember if we were in immersive mode |
| mPanicTime = time; |
| } else { |
| mPanicTime = 0; |
| } |
| return false; |
| } |
| |
| void confirmCurrentPrompt() { |
| if (mClingWindow != null) { |
| if (DEBUG) Slog.d(TAG, "confirmCurrentPrompt()"); |
| mHandler.post(mConfirm); |
| } |
| } |
| |
| private void handleHide() { |
| if (mClingWindow != null) { |
| if (DEBUG) Slog.d(TAG, "Hiding immersive mode confirmation"); |
| getWindowManager().removeView(mClingWindow); |
| mClingWindow = null; |
| } |
| } |
| |
| private WindowManager.LayoutParams getClingWindowLayoutParams() { |
| final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| ViewGroup.LayoutParams.MATCH_PARENT, |
| WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL, |
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
| | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
| | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, |
| PixelFormat.TRANSLUCENT); |
| lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars()); |
| lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; |
| lp.setTitle("ImmersiveModeConfirmation"); |
| lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation; |
| lp.token = getWindowToken(); |
| return lp; |
| } |
| |
| private FrameLayout.LayoutParams getBubbleLayoutParams() { |
| return new FrameLayout.LayoutParams( |
| mContext.getResources().getDimensionPixelSize( |
| R.dimen.immersive_mode_cling_width), |
| ViewGroup.LayoutParams.WRAP_CONTENT, |
| Gravity.CENTER_HORIZONTAL | Gravity.TOP); |
| } |
| |
| /** |
| * @return the window token that's used by all ImmersiveModeConfirmation windows. |
| */ |
| IBinder getWindowToken() { |
| return mWindowToken; |
| } |
| |
| private class ClingWindowView extends FrameLayout { |
| private static final int BGCOLOR = 0x80000000; |
| private static final int OFFSET_DP = 96; |
| private static final int ANIMATION_DURATION = 250; |
| |
| private final Runnable mConfirm; |
| private final ColorDrawable mColor = new ColorDrawable(0); |
| private final Interpolator mInterpolator; |
| private ValueAnimator mColorAnim; |
| private ViewGroup mClingLayout; |
| |
| private Runnable mUpdateLayoutRunnable = new Runnable() { |
| @Override |
| public void run() { |
| if (mClingLayout != null && mClingLayout.getParent() != null) { |
| mClingLayout.setLayoutParams(getBubbleLayoutParams()); |
| } |
| } |
| }; |
| |
| private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = |
| new ViewTreeObserver.OnComputeInternalInsetsListener() { |
| private final int[] mTmpInt2 = new int[2]; |
| |
| @Override |
| public void onComputeInternalInsets( |
| ViewTreeObserver.InternalInsetsInfo inoutInfo) { |
| // Set touchable region to cover the cling layout. |
| mClingLayout.getLocationInWindow(mTmpInt2); |
| inoutInfo.setTouchableInsets( |
| ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); |
| inoutInfo.touchableRegion.set( |
| mTmpInt2[0], |
| mTmpInt2[1], |
| mTmpInt2[0] + mClingLayout.getWidth(), |
| mTmpInt2[1] + mClingLayout.getHeight()); |
| } |
| }; |
| |
| private BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { |
| post(mUpdateLayoutRunnable); |
| } |
| } |
| }; |
| |
| ClingWindowView(Context context, Runnable confirm) { |
| super(context); |
| mConfirm = confirm; |
| setBackground(mColor); |
| setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); |
| mInterpolator = AnimationUtils |
| .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); |
| } |
| |
| @Override |
| public void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| DisplayMetrics metrics = new DisplayMetrics(); |
| mContext.getDisplay().getMetrics(metrics); |
| float density = metrics.density; |
| |
| getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener); |
| |
| // create the confirmation cling |
| mClingLayout = (ViewGroup) |
| View.inflate(getContext(), R.layout.immersive_mode_cling, null); |
| |
| final Button ok = mClingLayout.findViewById(R.id.ok); |
| ok.setOnClickListener(new OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| mConfirm.run(); |
| } |
| }); |
| addView(mClingLayout, getBubbleLayoutParams()); |
| |
| if (ActivityManager.isHighEndGfx()) { |
| final View cling = mClingLayout; |
| cling.setAlpha(0f); |
| cling.setTranslationY(-OFFSET_DP * density); |
| |
| postOnAnimation(new Runnable() { |
| @Override |
| public void run() { |
| cling.animate() |
| .alpha(1f) |
| .translationY(0) |
| .setDuration(ANIMATION_DURATION) |
| .setInterpolator(mInterpolator) |
| .withLayer() |
| .start(); |
| |
| mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR); |
| mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| final int c = (Integer) animation.getAnimatedValue(); |
| mColor.setColor(c); |
| } |
| }); |
| mColorAnim.setDuration(ANIMATION_DURATION); |
| mColorAnim.setInterpolator(mInterpolator); |
| mColorAnim.start(); |
| } |
| }); |
| } else { |
| mColor.setColor(BGCOLOR); |
| } |
| |
| mContext.registerReceiver(mReceiver, |
| new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| mContext.unregisterReceiver(mReceiver); |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent motion) { |
| return true; |
| } |
| } |
| |
| /** |
| * DO HOLD THE WINDOW MANAGER LOCK WHEN CALLING THIS METHOD |
| * The reason why we add this method is to avoid the deadlock of WMG->WMS and WMS->WMG |
| * when ImmersiveModeConfirmation object is created. |
| */ |
| private WindowManager getWindowManager() { |
| if (mWindowManager == null) { |
| mWindowManager = (WindowManager) |
| mContext.getSystemService(Context.WINDOW_SERVICE); |
| } |
| return mWindowManager; |
| } |
| |
| private void handleShow() { |
| if (DEBUG) Slog.d(TAG, "Showing immersive mode confirmation"); |
| |
| mClingWindow = new ClingWindowView(mContext, mConfirm); |
| |
| // we will be hiding the nav bar, so layout as if it's already hidden |
| mClingWindow.setSystemUiVisibility( |
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); |
| |
| // show the confirmation |
| WindowManager.LayoutParams lp = getClingWindowLayoutParams(); |
| getWindowManager().addView(mClingWindow, lp); |
| } |
| |
| private final Runnable mConfirm = new Runnable() { |
| @Override |
| public void run() { |
| if (DEBUG) Slog.d(TAG, "mConfirm.run()"); |
| if (!sConfirmed) { |
| sConfirmed = true; |
| saveSetting(mContext); |
| } |
| handleHide(); |
| } |
| }; |
| |
| private final class H extends Handler { |
| private static final int SHOW = 1; |
| private static final int HIDE = 2; |
| |
| H(Looper looper) { |
| super(looper); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch(msg.what) { |
| case SHOW: |
| handleShow(); |
| break; |
| case HIDE: |
| handleHide(); |
| break; |
| } |
| } |
| } |
| |
| void onVrStateChangedLw(boolean enabled) { |
| mVrModeEnabled = enabled; |
| if (mVrModeEnabled) { |
| mHandler.removeMessages(H.SHOW); |
| mHandler.sendEmptyMessage(H.HIDE); |
| } |
| } |
| |
| void onLockTaskModeChangedLw(int lockTaskState) { |
| mLockTaskState = lockTaskState; |
| } |
| } |