/*
|
* 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.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.privateFlags |= WindowManager.LayoutParams.PRIVATE_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();
|
getWindowManager().getDefaultDisplay().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;
|
}
|
}
|