Merge "Fullscreen user switching dialog with animations." into udc-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
0110ea1392
@@ -127,6 +127,7 @@ public class UserLifecycleTests {
|
||||
private BroadcastWaiter mBroadcastWaiter;
|
||||
private UserSwitchWaiter mUserSwitchWaiter;
|
||||
private String mUserSwitchTimeoutMs;
|
||||
private String mDisableUserSwitchingDialogAnimations;
|
||||
|
||||
private final BenchmarkRunner mRunner = new BenchmarkRunner();
|
||||
@Rule
|
||||
@@ -153,16 +154,17 @@ public class UserLifecycleTests {
|
||||
Log.w(TAG, "WARNING: Tests are being run from user " + mAm.getCurrentUser()
|
||||
+ " rather than the system user");
|
||||
}
|
||||
mUserSwitchTimeoutMs = setSystemProperty("debug.usercontroller.user_switch_timeout_ms",
|
||||
"100000");
|
||||
if (TextUtils.isEmpty(mUserSwitchTimeoutMs)) {
|
||||
mUserSwitchTimeoutMs = "invalid";
|
||||
}
|
||||
mUserSwitchTimeoutMs = setSystemProperty(
|
||||
"debug.usercontroller.user_switch_timeout_ms", "100000");
|
||||
mDisableUserSwitchingDialogAnimations = setSystemProperty(
|
||||
"debug.usercontroller.disable_user_switching_dialog_animations", "true");
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
setSystemProperty("debug.usercontroller.user_switch_timeout_ms", mUserSwitchTimeoutMs);
|
||||
setSystemProperty("debug.usercontroller.disable_user_switching_dialog_animations",
|
||||
mDisableUserSwitchingDialogAnimations);
|
||||
mBroadcastWaiter.close();
|
||||
mUserSwitchWaiter.close();
|
||||
for (int userId : mUsersToRemove) {
|
||||
@@ -1538,7 +1540,7 @@ public class UserLifecycleTests {
|
||||
private String setSystemProperty(String name, String value) throws Exception {
|
||||
final String oldValue = ShellHelper.runShellCommand("getprop " + name);
|
||||
assertEquals("", ShellHelper.runShellCommand("setprop " + name + " " + value));
|
||||
return oldValue;
|
||||
return TextUtils.firstNotEmpty(oldValue, "invalid");
|
||||
}
|
||||
|
||||
private void waitForBroadcastIdle() {
|
||||
|
||||
55
core/res/res/drawable/loading_spinner.xml
Normal file
55
core/res/res/drawable/loading_spinner.xml
Normal file
@@ -0,0 +1,55 @@
|
||||
<!-- Copyright (C) 2023 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.
|
||||
-->
|
||||
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt">
|
||||
<aapt:attr name="android:drawable">
|
||||
<vector android:height="230dp" android:width="230dp" android:viewportHeight="230"
|
||||
android:viewportWidth="230">
|
||||
<group android:name="_R_G">
|
||||
<group android:name="_R_G_L_0_G" android:translateX="100.621"
|
||||
android:translateY="102.621">
|
||||
<path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round" android:strokeLineJoin="round"
|
||||
android:strokeWidth="8" android:strokeAlpha="1" android:trimPathStart="0"
|
||||
android:trimPathEnd="0" android:trimPathOffset="0"
|
||||
android:pathData=" M14.38 -93.62 C72.88,-93.62 120.38,-46.12 120.38,12.38 C120.38,70.88 72.88,118.38 14.38,118.38 C-44.12,118.38 -91.62,70.88 -91.62,12.38 C-91.62,-46.12 -44.12,-93.62 14.38,-93.62c "/>
|
||||
</group>
|
||||
</group>
|
||||
<group android:name="time_group"/>
|
||||
</vector>
|
||||
</aapt:attr>
|
||||
<target android:name="_R_G_L_0_G_D_0_P_0">
|
||||
<aapt:attr name="android:animation">
|
||||
<set android:ordering="together">
|
||||
<objectAnimator android:propertyName="trimPathEnd" android:duration="350"
|
||||
android:startOffset="0" android:valueFrom="0" android:valueTo="1"
|
||||
android:valueType="floatType">
|
||||
<aapt:attr name="android:interpolator">
|
||||
<pathInterpolator android:pathData="M 0.0,0.0 c0.6,0 0.4,1 1.0,1.0"/>
|
||||
</aapt:attr>
|
||||
</objectAnimator>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
<target android:name="time_group">
|
||||
<aapt:attr name="android:animation">
|
||||
<set android:ordering="together">
|
||||
<objectAnimator android:propertyName="translateX" android:duration="517"
|
||||
android:startOffset="0" android:valueFrom="0" android:valueTo="1"
|
||||
android:valueType="floatType"/>
|
||||
</set>
|
||||
</aapt:attr>
|
||||
</target>
|
||||
</animated-vector>
|
||||
@@ -14,17 +14,48 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/content"
|
||||
android:background="?attr/colorBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/message"
|
||||
style="?attr/textAppearanceListItem"
|
||||
android:background="?attr/colorSurface"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:drawablePadding="12dp"
|
||||
android:drawableTint="?attr/textColorPrimary"
|
||||
android:paddingStart="?attr/dialogPreferredPadding"
|
||||
android:paddingEnd="?attr/dialogPreferredPadding"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="24dp" />
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="77dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="242dp"
|
||||
android:layout_height="242dp"
|
||||
android:layout_gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="26dp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="6dp"
|
||||
android:src="@drawable/loading_spinner" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/message"
|
||||
style="?attr/textAppearanceListItem"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="20sp"
|
||||
android:textAlignment="center"
|
||||
android:drawableTint="?attr/textColorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -141,6 +141,7 @@ import java.util.Objects;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Helper class for {@link ActivityManagerService} responsible for multi-user functionality.
|
||||
@@ -157,7 +158,7 @@ class UserController implements Handler.Callback {
|
||||
private static final String TAG = TAG_WITH_CLASS_NAME ? "UserController" : TAG_AM;
|
||||
|
||||
// Amount of time we wait for observers to handle a user switch before
|
||||
// giving up on them and unfreezing the screen.
|
||||
// giving up on them and dismissing the user switching dialog.
|
||||
static final int DEFAULT_USER_SWITCH_TIMEOUT_MS = 3 * 1000;
|
||||
|
||||
/**
|
||||
@@ -207,7 +208,7 @@ class UserController implements Handler.Callback {
|
||||
/**
|
||||
* Amount of time waited for {@link WindowManagerService#dismissKeyguard} callbacks to be
|
||||
* called after dismissing the keyguard.
|
||||
* Otherwise, we should move on to unfreeze the screen {@link #unfreezeScreen}
|
||||
* Otherwise, we should move on to dismiss the dialog {@link #dismissUserSwitchDialog()}
|
||||
* and report user switch is complete {@link #REPORT_USER_SWITCH_COMPLETE_MSG}.
|
||||
*/
|
||||
private static final int DISMISS_KEYGUARD_TIMEOUT_MS = 2 * 1000;
|
||||
@@ -1695,14 +1696,6 @@ class UserController implements Handler.Callback {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (foreground && isUserSwitchUiEnabled()) {
|
||||
t.traceBegin("startFreezingScreen");
|
||||
mInjector.getWindowManager().startFreezingScreen(
|
||||
R.anim.screen_user_exit, R.anim.screen_user_enter);
|
||||
t.traceEnd();
|
||||
}
|
||||
dismissUserSwitchDialog(); // so that we don't hold a reference to mUserSwitchingDialog
|
||||
|
||||
boolean needStart = false;
|
||||
boolean updateUmState = false;
|
||||
UserState uss;
|
||||
@@ -1877,7 +1870,7 @@ class UserController implements Handler.Callback {
|
||||
if (!success) {
|
||||
mInjector.getWindowManager().setSwitchingUser(false);
|
||||
mTargetUserId = UserHandle.USER_NULL;
|
||||
dismissUserSwitchDialog();
|
||||
dismissUserSwitchDialog(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2015,22 +2008,26 @@ class UserController implements Handler.Callback {
|
||||
mUiHandler.sendMessage(mUiHandler.obtainMessage(
|
||||
START_USER_SWITCH_UI_MSG, userNames));
|
||||
} else {
|
||||
mHandler.removeMessages(START_USER_SWITCH_FG_MSG);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(
|
||||
START_USER_SWITCH_FG_MSG, targetUserId, 0));
|
||||
sendStartUserSwitchFgMessage(targetUserId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void dismissUserSwitchDialog() {
|
||||
mInjector.dismissUserSwitchingDialog();
|
||||
private void sendStartUserSwitchFgMessage(int targetUserId) {
|
||||
mHandler.removeMessages(START_USER_SWITCH_FG_MSG);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(START_USER_SWITCH_FG_MSG, targetUserId, 0));
|
||||
}
|
||||
|
||||
private void dismissUserSwitchDialog(Runnable onDismissed) {
|
||||
mInjector.dismissUserSwitchingDialog(onDismissed);
|
||||
}
|
||||
|
||||
private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
|
||||
// The dialog will show and then initiate the user switch by calling startUserInForeground
|
||||
mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second,
|
||||
getSwitchingFromSystemUserMessageUnchecked(),
|
||||
getSwitchingToSystemUserMessageUnchecked());
|
||||
getSwitchingToSystemUserMessageUnchecked(),
|
||||
/* onShown= */ () -> sendStartUserSwitchFgMessage(fromToUserPair.second.id));
|
||||
}
|
||||
|
||||
private void dispatchForegroundProfileChanged(@UserIdInt int userId) {
|
||||
@@ -2236,7 +2233,7 @@ class UserController implements Handler.Callback {
|
||||
|
||||
EventLog.writeEvent(EventLogTags.UC_CONTINUE_USER_SWITCH, oldUserId, newUserId);
|
||||
|
||||
// Do the keyguard dismiss and unfreeze later
|
||||
// Do the keyguard dismiss and dismiss the user switching dialog later
|
||||
mHandler.removeMessages(COMPLETE_USER_SWITCH_MSG);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(
|
||||
COMPLETE_USER_SWITCH_MSG, oldUserId, newUserId));
|
||||
@@ -2251,35 +2248,31 @@ class UserController implements Handler.Callback {
|
||||
@VisibleForTesting
|
||||
void completeUserSwitch(int oldUserId, int newUserId) {
|
||||
final boolean isUserSwitchUiEnabled = isUserSwitchUiEnabled();
|
||||
final Runnable runnable = () -> {
|
||||
if (isUserSwitchUiEnabled) {
|
||||
unfreezeScreen();
|
||||
}
|
||||
mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(
|
||||
REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
|
||||
};
|
||||
|
||||
// If there is no challenge set, dismiss the keyguard right away
|
||||
if (isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId)) {
|
||||
// Wait until the keyguard is dismissed to unfreeze
|
||||
mInjector.dismissKeyguard(runnable);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
// serialize each conditional step
|
||||
await(
|
||||
// STEP 1 - If there is no challenge set, dismiss the keyguard right away
|
||||
isUserSwitchUiEnabled && !mInjector.getKeyguardManager().isDeviceSecure(newUserId),
|
||||
mInjector::dismissKeyguard,
|
||||
() -> await(
|
||||
// STEP 2 - If user switch ui was enabled, dismiss user switch dialog
|
||||
isUserSwitchUiEnabled,
|
||||
this::dismissUserSwitchDialog,
|
||||
() -> {
|
||||
// STEP 3 - Send REPORT_USER_SWITCH_COMPLETE_MSG to broadcast
|
||||
// ACTION_USER_SWITCHED & call UserSwitchObservers.onUserSwitchComplete
|
||||
mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
|
||||
mHandler.sendMessage(mHandler.obtainMessage(
|
||||
REPORT_USER_SWITCH_COMPLETE_MSG, oldUserId, newUserId));
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell WindowManager we're ready to unfreeze the screen, at its leisure. Note that there is
|
||||
* likely a lot going on, and WM won't unfreeze until the drawing is all done, so
|
||||
* the actual unfreeze may still not happen for a long time; this is expected.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void unfreezeScreen() {
|
||||
TimingsTraceAndSlog t = new TimingsTraceAndSlog();
|
||||
t.traceBegin("stopFreezingScreen");
|
||||
mInjector.getWindowManager().stopFreezingScreen();
|
||||
t.traceEnd();
|
||||
private void await(boolean condition, Consumer<Runnable> conditionalStep, Runnable nextStep) {
|
||||
if (condition) {
|
||||
conditionalStep.accept(nextStep);
|
||||
} else {
|
||||
nextStep.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void moveUserToForeground(UserState uss, int newUserId) {
|
||||
@@ -3731,17 +3724,18 @@ class UserController implements Handler.Callback {
|
||||
mService.mCpHelper.installEncryptionUnawareProviders(userId);
|
||||
}
|
||||
|
||||
void dismissUserSwitchingDialog() {
|
||||
void dismissUserSwitchingDialog(@Nullable Runnable onDismissed) {
|
||||
synchronized (mUserSwitchingDialogLock) {
|
||||
if (mUserSwitchingDialog != null) {
|
||||
mUserSwitchingDialog.dismiss();
|
||||
mUserSwitchingDialog.dismiss(onDismissed);
|
||||
mUserSwitchingDialog = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
|
||||
String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
|
||||
String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
|
||||
@NonNull Runnable onShown) {
|
||||
if (mService.mContext.getPackageManager()
|
||||
.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
|
||||
// config_customUserSwitchUi is set to true on Automotive as CarSystemUI is
|
||||
@@ -3751,11 +3745,10 @@ class UserController implements Handler.Callback {
|
||||
+ "condition if it's shown by CarSystemUI as well");
|
||||
}
|
||||
synchronized (mUserSwitchingDialogLock) {
|
||||
dismissUserSwitchingDialog();
|
||||
mUserSwitchingDialog = new UserSwitchingDialog(mService, mService.mContext,
|
||||
fromUser, toUser, true /* above system */, switchingFromSystemUserMessage,
|
||||
switchingToSystemUserMessage);
|
||||
mUserSwitchingDialog.show();
|
||||
dismissUserSwitchingDialog(null);
|
||||
mUserSwitchingDialog = new UserSwitchingDialog(mService.mContext, fromUser, toUser,
|
||||
switchingFromSystemUserMessage, switchingToSystemUserMessage);
|
||||
mUserSwitchingDialog.show(onShown);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,160 +16,258 @@
|
||||
|
||||
package com.android.server.am;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.annotation.UserIdInt;
|
||||
import android.app.ActivityManager;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.pm.UserInfo;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapShader;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Animatable2;
|
||||
import android.graphics.drawable.AnimatedVectorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemProperties;
|
||||
import android.os.Trace;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.util.Slog;
|
||||
import android.view.LayoutInflater;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.internal.R;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.internal.util.ObjectUtils;
|
||||
import com.android.internal.util.UserIcons;
|
||||
|
||||
/**
|
||||
* Dialog to show when a user switch it about to happen. The intent is to snapshot the screen
|
||||
* immediately after the dialog shows so that the user is informed that something is happening
|
||||
* in the background rather than just freeze the screen and not know if the user-switch affordance
|
||||
* was being handled.
|
||||
* Dialog to show during the user switch. This dialog shows target user's name and their profile
|
||||
* picture with a circular spinner animation around it if the animations for this dialog are not
|
||||
* disabled. And covers the whole screen so that all the UI jank caused by the switch are hidden.
|
||||
*/
|
||||
class UserSwitchingDialog extends AlertDialog
|
||||
implements ViewTreeObserver.OnWindowShownListener {
|
||||
private static final String TAG = "ActivityManagerUserSwitchingDialog";
|
||||
|
||||
// Time to wait for the onWindowShown() callback before continuing the user switch
|
||||
private static final int WINDOW_SHOWN_TIMEOUT_MS = 3000;
|
||||
class UserSwitchingDialog extends Dialog {
|
||||
private static final String TAG = "UserSwitchingDialog";
|
||||
private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
|
||||
|
||||
// User switching doesn't happen that frequently, so it doesn't hurt to have it always on
|
||||
protected static final boolean DEBUG = true;
|
||||
private static final long DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS = 300;
|
||||
private final boolean mDisableAnimations;
|
||||
|
||||
private final ActivityManagerService mService;
|
||||
private final int mUserId;
|
||||
private static final int MSG_START_USER = 1;
|
||||
@GuardedBy("this")
|
||||
private boolean mStartedUser;
|
||||
final protected UserInfo mOldUser;
|
||||
final protected UserInfo mNewUser;
|
||||
final private String mSwitchingFromSystemUserMessage;
|
||||
final private String mSwitchingToSystemUserMessage;
|
||||
final protected Context mContext;
|
||||
protected final UserInfo mOldUser;
|
||||
protected final UserInfo mNewUser;
|
||||
private final String mSwitchingFromSystemUserMessage;
|
||||
private final String mSwitchingToSystemUserMessage;
|
||||
protected final Context mContext;
|
||||
private final int mTraceCookie;
|
||||
|
||||
public UserSwitchingDialog(ActivityManagerService service, Context context, UserInfo oldUser,
|
||||
UserInfo newUser, boolean aboveSystem, String switchingFromSystemUserMessage,
|
||||
String switchingToSystemUserMessage) {
|
||||
super(context);
|
||||
UserSwitchingDialog(Context context, UserInfo oldUser, UserInfo newUser,
|
||||
String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
|
||||
// TODO(b/278857848): Make full screen user switcher cover top part of the screen as well.
|
||||
// This problem is seen only on phones, it works fine on tablets.
|
||||
super(context, R.style.Theme_Material_NoActionBar_Fullscreen);
|
||||
|
||||
mContext = context;
|
||||
mService = service;
|
||||
mUserId = newUser.id;
|
||||
mOldUser = oldUser;
|
||||
mNewUser = newUser;
|
||||
mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
|
||||
mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
|
||||
mDisableAnimations = ActivityManager.isLowRamDeviceStatic() || SystemProperties.getBoolean(
|
||||
"debug.usercontroller.disable_user_switching_dialog_animations", false);
|
||||
mTraceCookie = UserHandle.MAX_SECONDARY_USER_ID * oldUser.id + newUser.id;
|
||||
|
||||
inflateContent();
|
||||
configureWindow();
|
||||
}
|
||||
|
||||
if (aboveSystem) {
|
||||
getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
|
||||
}
|
||||
|
||||
WindowManager.LayoutParams attrs = getWindow().getAttributes();
|
||||
private void configureWindow() {
|
||||
final Window window = getWindow();
|
||||
final WindowManager.LayoutParams attrs = window.getAttributes();
|
||||
attrs.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SYSTEM_ERROR |
|
||||
WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
|
||||
getWindow().setAttributes(attrs);
|
||||
WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
|
||||
window.setAttributes(attrs);
|
||||
window.setBackgroundDrawableResource(android.R.color.transparent);
|
||||
window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_ERROR);
|
||||
}
|
||||
|
||||
void inflateContent() {
|
||||
// Set up the dialog contents
|
||||
setCancelable(false);
|
||||
Resources res = getContext().getResources();
|
||||
// Custom view due to alignment and font size requirements
|
||||
TextView view = (TextView) LayoutInflater.from(getContext()).inflate(
|
||||
R.layout.user_switching_dialog, null);
|
||||
setContentView(R.layout.user_switching_dialog);
|
||||
|
||||
String viewMessage = null;
|
||||
if (UserManager.isDeviceInDemoMode(mContext)) {
|
||||
if (mOldUser.isDemo()) {
|
||||
viewMessage = res.getString(R.string.demo_restarting_message);
|
||||
} else {
|
||||
viewMessage = res.getString(R.string.demo_starting_message);
|
||||
}
|
||||
} else {
|
||||
if (mOldUser.id == UserHandle.USER_SYSTEM) {
|
||||
viewMessage = mSwitchingFromSystemUserMessage;
|
||||
} else if (mNewUser.id == UserHandle.USER_SYSTEM) {
|
||||
viewMessage = mSwitchingToSystemUserMessage;
|
||||
}
|
||||
|
||||
// If switchingFromSystemUserMessage or switchingToSystemUserMessage is null, fallback
|
||||
// to system message.
|
||||
if (viewMessage == null) {
|
||||
viewMessage = res.getString(R.string.user_switching_message, mNewUser.name);
|
||||
}
|
||||
|
||||
view.setCompoundDrawablesWithIntrinsicBounds(null,
|
||||
getContext().getDrawable(R.drawable.ic_swap_horiz), null, null);
|
||||
final TextView textView = findViewById(R.id.message);
|
||||
if (textView != null) {
|
||||
final String message = getTextMessage();
|
||||
textView.setAccessibilityPaneTitle(message);
|
||||
textView.setText(message);
|
||||
}
|
||||
view.setAccessibilityPaneTitle(viewMessage);
|
||||
view.setText(viewMessage);
|
||||
setView(view);
|
||||
|
||||
final ImageView imageView = findViewById(R.id.icon);
|
||||
if (imageView != null) {
|
||||
imageView.setImageBitmap(getUserIconRounded());
|
||||
}
|
||||
|
||||
final ImageView progressCircular = findViewById(R.id.progress_circular);
|
||||
if (progressCircular != null) {
|
||||
if (mDisableAnimations) {
|
||||
progressCircular.setVisibility(View.GONE);
|
||||
} else {
|
||||
final TypedValue value = new TypedValue();
|
||||
getContext().getTheme().resolveAttribute(R.attr.colorAccentPrimary, value, true);
|
||||
progressCircular.setColorFilter(value.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap getUserIconRounded() {
|
||||
final Bitmap bmp = ObjectUtils.getOrElse(BitmapFactory.decodeFile(mNewUser.iconPath),
|
||||
defaultUserIcon(mNewUser.id));
|
||||
final int w = bmp.getWidth();
|
||||
final int h = bmp.getHeight();
|
||||
final Bitmap bmpRounded = Bitmap.createBitmap(w, h, bmp.getConfig());
|
||||
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setShader(new BitmapShader(bmp, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP));
|
||||
new Canvas(bmpRounded).drawRoundRect((new RectF(0, 0, w, h)), w / 2f, h / 2f, paint);
|
||||
return bmpRounded;
|
||||
}
|
||||
|
||||
private Bitmap defaultUserIcon(@UserIdInt int userId) {
|
||||
final Resources res = getContext().getResources();
|
||||
final Drawable icon = UserIcons.getDefaultUserIcon(res, userId, /* light= */ false);
|
||||
return UserIcons.convertToBitmapAtUserIconSize(res, icon);
|
||||
}
|
||||
|
||||
private String getTextMessage() {
|
||||
final Resources res = getContext().getResources();
|
||||
|
||||
if (UserManager.isDeviceInDemoMode(mContext)) {
|
||||
return res.getString(mOldUser.isDemo()
|
||||
? R.string.demo_restarting_message
|
||||
: R.string.demo_starting_message);
|
||||
}
|
||||
|
||||
final String message =
|
||||
mOldUser.id == UserHandle.USER_SYSTEM ? mSwitchingFromSystemUserMessage
|
||||
: mNewUser.id == UserHandle.USER_SYSTEM ? mSwitchingToSystemUserMessage : null;
|
||||
|
||||
return message != null ? message
|
||||
// If switchingFromSystemUserMessage or switchingToSystemUserMessage is null,
|
||||
// fallback to system message.
|
||||
: res.getString(R.string.user_switching_message, mNewUser.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
if (DEBUG) Slog.d(TAG, "show called");
|
||||
asyncTraceBegin("", 0);
|
||||
super.show();
|
||||
final View decorView = getWindow().getDecorView();
|
||||
if (decorView != null) {
|
||||
decorView.getViewTreeObserver().addOnWindowShownListener(this);
|
||||
}
|
||||
// Add a timeout as a safeguard, in case a race in screen on/off causes the window
|
||||
// callback to never come.
|
||||
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_USER),
|
||||
WINDOW_SHOWN_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowShown() {
|
||||
if (DEBUG) Slog.d(TAG, "onWindowShown called");
|
||||
startUser();
|
||||
public void dismiss() {
|
||||
super.dismiss();
|
||||
asyncTraceEnd("", 0);
|
||||
}
|
||||
|
||||
void startUser() {
|
||||
synchronized (this) {
|
||||
if (!mStartedUser) {
|
||||
Slog.i(TAG, "starting user " + mUserId);
|
||||
mService.mUserController.startUserInForeground(mUserId);
|
||||
public void show(@NonNull Runnable onShown) {
|
||||
if (DEBUG) Slog.d(TAG, "show called");
|
||||
show();
|
||||
|
||||
if (mDisableAnimations) {
|
||||
onShown.run();
|
||||
} else {
|
||||
startShowAnimation(onShown);
|
||||
}
|
||||
}
|
||||
|
||||
public void dismiss(@Nullable Runnable onDismissed) {
|
||||
if (DEBUG) Slog.d(TAG, "dismiss called");
|
||||
|
||||
if (onDismissed == null) {
|
||||
// no animation needed
|
||||
dismiss();
|
||||
} else if (mDisableAnimations) {
|
||||
dismiss();
|
||||
onDismissed.run();
|
||||
} else {
|
||||
startDismissAnimation(() -> {
|
||||
dismiss();
|
||||
mStartedUser = true;
|
||||
final View decorView = getWindow().getDecorView();
|
||||
if (decorView != null) {
|
||||
decorView.getViewTreeObserver().removeOnWindowShownListener(this);
|
||||
}
|
||||
mHandler.removeMessages(MSG_START_USER);
|
||||
} else {
|
||||
Slog.i(TAG, "user " + mUserId + " already started");
|
||||
}
|
||||
onDismissed.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final Handler mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_START_USER:
|
||||
Slog.w(TAG, "user switch window not shown in "
|
||||
+ WINDOW_SHOWN_TIMEOUT_MS + " ms");
|
||||
startUser();
|
||||
break;
|
||||
private void startShowAnimation(Runnable onAnimationEnd) {
|
||||
asyncTraceBegin("-showAnimation", 1);
|
||||
startDialogAnimation(new AlphaAnimation(0, 1), () -> {
|
||||
asyncTraceEnd("-showAnimation", 1);
|
||||
|
||||
asyncTraceBegin("-spinnerAnimation", 2);
|
||||
startProgressAnimation(() -> {
|
||||
asyncTraceEnd("-spinnerAnimation", 2);
|
||||
|
||||
onAnimationEnd.run();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void startDismissAnimation(Runnable onAnimationEnd) {
|
||||
asyncTraceBegin("-dismissAnimation", 3);
|
||||
startDialogAnimation(new AlphaAnimation(1, 0), () -> {
|
||||
asyncTraceEnd("-dismissAnimation", 3);
|
||||
|
||||
onAnimationEnd.run();
|
||||
});
|
||||
}
|
||||
|
||||
private void startProgressAnimation(Runnable onAnimationEnd) {
|
||||
final ImageView progressCircular = findViewById(R.id.progress_circular);
|
||||
final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) progressCircular.getDrawable();
|
||||
avd.registerAnimationCallback(new Animatable2.AnimationCallback() {
|
||||
@Override
|
||||
public void onAnimationEnd(Drawable drawable) {
|
||||
onAnimationEnd.run();
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
avd.start();
|
||||
}
|
||||
|
||||
private void startDialogAnimation(Animation animation, Runnable onAnimationEnd) {
|
||||
animation.setDuration(DIALOG_SHOW_HIDE_ANIMATION_DURATION_MS);
|
||||
animation.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
onAnimationEnd.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
|
||||
}
|
||||
});
|
||||
findViewById(R.id.content).startAnimation(animation);
|
||||
}
|
||||
|
||||
private void asyncTraceBegin(String subTag, int subCookie) {
|
||||
Trace.asyncTraceBegin(TRACE_TAG, TAG + subTag, mTraceCookie + subCookie);
|
||||
}
|
||||
|
||||
private void asyncTraceEnd(String subTag, int subCookie) {
|
||||
Trace.asyncTraceEnd(TRACE_TAG, TAG + subTag, mTraceCookie + subCookie);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,8 +211,7 @@ public class UserControllerTest {
|
||||
@Test
|
||||
public void testStartUser_foreground() {
|
||||
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
|
||||
verify(mInjector.getWindowManager()).startFreezingScreen(anyInt(), anyInt());
|
||||
verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
|
||||
verify(mInjector, never()).dismissUserSwitchingDialog(any());
|
||||
verify(mInjector.getWindowManager(), times(1)).setSwitchingUser(anyBoolean());
|
||||
verify(mInjector.getWindowManager()).setSwitchingUser(true);
|
||||
verify(mInjector).clearAllLockedTasks(anyString());
|
||||
@@ -224,7 +223,8 @@ public class UserControllerTest {
|
||||
public void testStartUser_background() {
|
||||
boolean started = mUserController.startUser(TEST_USER_ID, USER_START_MODE_BACKGROUND);
|
||||
assertWithMessage("startUser(%s, foreground=false)", TEST_USER_ID).that(started).isTrue();
|
||||
verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
|
||||
verify(mInjector, never()).showUserSwitchingDialog(
|
||||
any(), any(), anyString(), anyString(), any());
|
||||
verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
|
||||
verify(mInjector, never()).clearAllLockedTasks(anyString());
|
||||
startBackgroundUserAssertions();
|
||||
@@ -276,7 +276,8 @@ public class UserControllerTest {
|
||||
assertWithMessage("startUserOnDisplay(%s, %s)", TEST_USER_ID, 42).that(started).isTrue();
|
||||
verifyUserAssignedToDisplay(TEST_USER_ID, 42);
|
||||
|
||||
verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
|
||||
verify(mInjector, never()).showUserSwitchingDialog(
|
||||
any(), any(), anyString(), anyString(), any());
|
||||
verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
|
||||
verify(mInjector, never()).clearAllLockedTasks(anyString());
|
||||
startBackgroundUserAssertions();
|
||||
@@ -288,8 +289,9 @@ public class UserControllerTest {
|
||||
/* maxRunningUsers= */ 3, /* delayUserDataLocking= */ false);
|
||||
|
||||
mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND);
|
||||
verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
|
||||
verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
|
||||
verify(mInjector, never()).showUserSwitchingDialog(
|
||||
any(), any(), anyString(), anyString(), any());
|
||||
verify(mInjector, never()).dismissUserSwitchingDialog(any());
|
||||
verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
|
||||
startForegroundUserAssertions();
|
||||
}
|
||||
@@ -310,7 +312,8 @@ public class UserControllerTest {
|
||||
// Make sure no intents have been fired for pre-created users.
|
||||
assertTrue(mInjector.mSentIntents.isEmpty());
|
||||
|
||||
verify(mInjector.getWindowManager(), never()).startFreezingScreen(anyInt(), anyInt());
|
||||
verify(mInjector, never()).showUserSwitchingDialog(
|
||||
any(), any(), anyString(), anyString(), any());
|
||||
verify(mInjector.getWindowManager(), never()).setSwitchingUser(anyBoolean());
|
||||
verify(mInjector, never()).clearAllLockedTasks(anyString());
|
||||
|
||||
@@ -442,7 +445,7 @@ public class UserControllerTest {
|
||||
// Verify that continueUserSwitch worked as expected
|
||||
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
|
||||
verify(mInjector, times(0)).dismissKeyguard(any());
|
||||
verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
|
||||
verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
|
||||
continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
|
||||
verifySystemUserVisibilityChangesNeverNotified();
|
||||
}
|
||||
@@ -463,7 +466,7 @@ public class UserControllerTest {
|
||||
// Verify that continueUserSwitch worked as expected
|
||||
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
|
||||
verify(mInjector, times(1)).dismissKeyguard(any());
|
||||
verify(mInjector.getWindowManager(), times(1)).stopFreezingScreen();
|
||||
verify(mInjector, times(1)).dismissUserSwitchingDialog(any());
|
||||
continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
|
||||
verifySystemUserVisibilityChangesNeverNotified();
|
||||
}
|
||||
@@ -483,7 +486,7 @@ public class UserControllerTest {
|
||||
mInjector.mHandler.clearAllRecordedMessages();
|
||||
// Verify that continueUserSwitch worked as expected
|
||||
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
|
||||
verify(mInjector.getWindowManager(), never()).stopFreezingScreen();
|
||||
verify(mInjector, never()).dismissUserSwitchingDialog(any());
|
||||
continueUserSwitchAssertions(oldUserId, TEST_USER_ID, false);
|
||||
}
|
||||
|
||||
@@ -985,8 +988,7 @@ public class UserControllerTest {
|
||||
mInjector.mHandler.clearAllRecordedMessages();
|
||||
// Verify that continueUserSwitch worked as expected
|
||||
continueAndCompleteUserSwitch(userState, oldUserId, newUserId);
|
||||
verify(mInjector.getWindowManager(), times(expectedNumberOfCalls))
|
||||
.stopFreezingScreen();
|
||||
verify(mInjector, times(expectedNumberOfCalls)).dismissUserSwitchingDialog(any());
|
||||
continueUserSwitchAssertions(oldUserId, newUserId, expectOldUserStopping);
|
||||
}
|
||||
|
||||
@@ -1188,6 +1190,22 @@ public class UserControllerTest {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
|
||||
String switchingFromSystemUserMessage, String switchingToSystemUserMessage,
|
||||
Runnable onShown) {
|
||||
if (onShown != null) {
|
||||
onShown.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void dismissUserSwitchingDialog(Runnable onDismissed) {
|
||||
if (onDismissed != null) {
|
||||
onDismissed.run();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LockPatternUtils getLockPatternUtils() {
|
||||
return mLockPatternUtilsMock;
|
||||
|
||||
Reference in New Issue
Block a user