Fullscreen user switching dialog with animations.
Instead of freezing the screen during the user switch, with this CL we are introducing a full screen user switcher, which is showing target user's user name and profile photo with an animated spinner around it. Steps of a user switch: 1. 300ms - dialog show animation 2. 500ms - spinner animation around profile picture 3. Do the actual user switch 4. 300ms - dialog dismiss animation 5. End user switch (call UserSwitchObservers.onUserSwitchComplete, and send ACTION_USER_SWITCHED broadcast) Changes: * Step1 was already there. * Step2 is added between 1-3, which shows a nice smooth progress animation around profile picture, but increases the user switch duration by 500ms. * Step5 and Step4 were running at the same time before, now Step5 is postponed after the completion of Step4. Otherwise, dismiss animation becomes jerky. It was also jerky with unfreezing the screen, now that jerkiness is gone. * All animations are disabled for slower devices. see: ActivityManager.isLowRamDeviceStatic() Results: * We're increasing the user switch duration by 800ms. (Step2 + Step4) * We've a full screen user switcher with smooth animations. * There is no more jerkiness in any stage (showing, during, hiding) of the dialog. Notes: * Our intention was to run Step2 and Step3 simultaneously but it makes the spinner animation jerky. * We're disabling the dialog show/hide and spinner animations when running UserLifecycleTests. Bug: 269237775 Bug: 223090747 Test: atest FrameworksServicesTests:UserControllerTest Test: atest UserLifecycleTests Change-Id: I5e0132e19c8da25438c5dbfecdeddf475b18f7d4
This commit is contained in:
@@ -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