Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4eb7c3fe authored by Yasin Kilicdere's avatar Yasin Kilicdere
Browse files

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
parent 4eb6f77e
Loading
Loading
Loading
Loading
+8 −6
Original line number Diff line number Diff line
@@ -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 −0
Original line number Diff line number Diff line
<!-- 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>
 No newline at end of file
+42 −11
Original line number Diff line number Diff line
@@ -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">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        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:background="?attr/colorSurface"
                  android:layout_width="wrap_content"
        android:layout_height="match_parent"
        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:layout_height="wrap_content"
                  android:textSize="20sp"
                  android:textAlignment="center"
                  android:drawableTint="?attr/textColorPrimary" />

    </LinearLayout>
</FrameLayout>
+45 −52
Original line number Diff line number Diff line
@@ -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();
            }
        // 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));
        };

        // 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();
                        }
                ));
    }

    /**
     * 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);
            }
        }

+201 −103

File changed.

Preview size limit exceeded, changes collapsed.

Loading