diff options
author | 2022-02-07 11:03:06 -0500 | |
---|---|---|
committer | 2022-02-08 15:14:07 -0500 | |
commit | d04be1df3ce49734e44c0a2886173548c2b347a3 (patch) | |
tree | 054b4163062840b4e4423f9f4585589c6f06d12c | |
parent | 5c5cdad706ac04c5aab2dcdb533f4696c43d09f4 (diff) |
Fullscreen user switcher
Supports fullscreen user switcher launched from QS, and enabled by
config_enableFullscreenUserSwitcher. Allows adding guests, with
support for adding new users coming shortly. First round of work on
this feature.
Bug: 217365397
Test: atest UserSwitcherControllerTest
Change-Id: Ib81f44c9b950830a7d89b95cbaef53b7228ed5a4
17 files changed, 770 insertions, 26 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index c9bd3710ca79..6b6aa71d74c0 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -845,6 +845,18 @@ android:visibleToInstantApps="true"> </activity> + <activity android:name=".user.UserSwitcherActivity" + android:label="@string/accessibility_multi_user_switch_switcher" + android:theme="@style/Theme.UserSwitcherActivity" + android:excludeFromRecents="true" + android:showWhenLocked="true" + android:showForAllUsers="true" + android:finishOnTaskLaunch="true" + android:launchMode="singleInstance" + android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden" + android:visibleToInstantApps="true"> + </activity> + <receiver android:name=".controls.management.ControlsRequestReceiver" android:exported="true"> <intent-filter> diff --git a/packages/SystemUI/docs/user-switching.md b/packages/SystemUI/docs/user-switching.md new file mode 100644 index 000000000000..dcf66b943f1d --- /dev/null +++ b/packages/SystemUI/docs/user-switching.md @@ -0,0 +1,45 @@ +# User Switching + +Multiple users and the ability to switch between them is controlled by Settings -> System -> Multiple Users. + +## Entry Points + +### Quick Settings + +In the QS footer, an icon becomes available for users to tap on. The view and its onClick actions are handled by [MultiUserSwitchController][2]. Multiple visual implementations are currently in use; one for phones/foldables ([UserSwitchDialogController][6]) and one for tablets ([UserSwitcherActivity][5]). + +### Bouncer + +May allow changing or adding new users directly from they bouncer. See [KeyguardBouncer][1] + +### Keyguard affordance + +[KeyguardQsUserSwitchController][4] + +## Components + +All visual implementations should derive their logic and use the adapter specified in: + +### [UserSwitcherController][3] + +* Contains the current list of all system users +* Listens for relevant events and broadcasts to make sure this list stays up to date +* Manages user switching and dialogs for exiting from guest users +* Is settings aware regarding adding users from the lockscreen + +## Visual Components + +### [UserSwitcherActivity][5] + +A fullscreen user switching activity, supporting add guest/user actions if configured. + +### [UserSwitchDialogController][6] + +Renders user switching as a dialog over the current surface, and supports add guest user/actions if configured. + +[1]: /frameworks/base/packages/SystemUI/docs/keyguard/bouncer.md +[2]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserController.java +[3]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +[4]: /frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +[5]: /frameworks/base/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt +[6]: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index cbf4f83daeb5..dad4c19799af 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -113,9 +113,17 @@ <dimen name="bouncer_user_switcher_item_icon_size">28dp</dimen> <dimen name="bouncer_user_switcher_item_icon_padding">12dp</dimen> <dimen name="bouncer_user_switcher_width">248dp</dimen> - <dimen name="bouncer_user_switcher_icon_size">190dp</dimen> <dimen name="bouncer_user_switcher_popup_header_height">12dp</dimen> <dimen name="bouncer_user_switcher_popup_divider_height">4dp</dimen> <dimen name="bouncer_user_switcher_item_padding_vertical">10dp</dimen> <dimen name="bouncer_user_switcher_item_padding_horizontal">12dp</dimen> + + <!-- 2 * the margin + size should equal the plus_margin --> + <dimen name="user_switcher_icon_large_margin">16dp</dimen> + <dimen name="bouncer_user_switcher_icon_size">190dp</dimen> + <dimen name="bouncer_user_switcher_icon_size_plus_margin">222dp</dimen> + + <dimen name="user_switcher_icon_selected_width">8dp</dimen> + <dimen name="user_switcher_fullscreen_button_text_size">14sp</dimen> + <dimen name="user_switcher_fullscreen_button_padding">12dp</dimen> </resources> diff --git a/packages/SystemUI/res/drawable/user_switcher_icon_large.xml b/packages/SystemUI/res/drawable/user_switcher_icon_large.xml new file mode 100644 index 000000000000..b78b2216c9f9 --- /dev/null +++ b/packages/SystemUI/res/drawable/user_switcher_icon_large.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +* Copyright 2022, 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. +*/ +--> +<layer-list + xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/bouncer_user_switcher_icon_size_plus_margin" + android:height="@dimen/bouncer_user_switcher_icon_size_plus_margin"> + <!-- The final layer is inset, so it needs this background --> + <item> + <shape android:shape="oval"> + <solid android:color="@color/user_switcher_fullscreen_bg" /> + </shape> + </item> + <!-- When an item is selected, this layer will show a ring around the icon --> + <item> + <shape android:shape="oval"> + <stroke + android:width="@dimen/user_switcher_icon_selected_width" + android:color="@android:color/transparent" /> + </shape> + </item> + <!-- Where the user drawable/bitmap will be placed --> + <item + android:drawable="@drawable/kg_bg_avatar" + android:width="@dimen/bouncer_user_switcher_icon_size" + android:height="@dimen/bouncer_user_switcher_icon_size" + android:top="@dimen/user_switcher_icon_large_margin" + android:left="@dimen/user_switcher_icon_large_margin" + android:right="@dimen/user_switcher_icon_large_margin" + android:bottom="@dimen/user_switcher_icon_large_margin" /> +</layer-list> diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml new file mode 100644 index 000000000000..7b95cf3cfa34 --- /dev/null +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2022 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. +--> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/user_switcher_root" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginEnd="60dp" + android:layout_marginStart="60dp"> + + <androidx.constraintlayout.helper.widget.Flow + android:id="@+id/flow" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:flow_horizontalBias="0.5" + app:flow_verticalAlign="center" + app:flow_wrapMode="chain" + app:flow_horizontalGap="64dp" + app:flow_verticalGap="44dp" + app:flow_horizontalStyle="packed"/> + + <TextView + android:id="@+id/cancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + app:layout_constraintHeight_min="48dp" + app:layout_constraintEnd_toStartOf="@+id/add" + app:layout_constraintBottom_toBottomOf="parent" + android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding" + android:textSize="@dimen/user_switcher_fullscreen_button_text_size" + android:textColor="?androidprv:attr/colorAccentPrimary" + android:text="@string/cancel" /> + + <TextView + android:id="@+id/add" + android:visibility="gone" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + app:layout_constraintHeight_min="48dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:paddingHorizontal="@dimen/user_switcher_fullscreen_button_padding" + android:textSize="@dimen/user_switcher_fullscreen_button_text_size" + android:textColor="?androidprv:attr/colorAccentPrimary" + android:text="@string/add" /> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen_item.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen_item.xml new file mode 100644 index 000000000000..3319442a1a68 --- /dev/null +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen_item.xml @@ -0,0 +1,33 @@ +<!-- + ~ Copyright (C) 2022 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. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical"> + <ImageView + android:id="@+id/user_switcher_icon" + android:layout_gravity="center" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + <TextView + style="@style/Bouncer.UserSwitcher.Spinner.Item" + android:id="@+id/user_switcher_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@*android:color/text_color_primary_device_default_dark" + android:layout_gravity="center" /> +</LinearLayout> diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen_popup_item.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen_popup_item.xml new file mode 100644 index 000000000000..8d02429150f0 --- /dev/null +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen_popup_item.xml @@ -0,0 +1,47 @@ +<!-- + ~ Copyright (C) 2022 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. + --> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="18dp" + android:paddingStart="18dp" + android:paddingEnd="65dp"> + + <LinearLayout + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="start"> + + <ImageView + android:id="@+id/icon" + android:layout_gravity="center" + android:layout_width="20dp" + android:layout_height="20dp" + android:contentDescription="@null" + android:layout_marginEnd="10dp" /> + + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@*android:color/text_color_primary_device_default_dark" + android:textSize="14sp" + android:layout_gravity="start" /> + </LinearLayout> + +</LinearLayout> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 3ab569a19c0c..15147786e557 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -87,6 +87,7 @@ <color name="notification_section_clear_all_btn_color">@color/GM2_grey_700</color> <color name="keyguard_user_switcher_background_gradient_color">#77000000</color> + <color name="user_switcher_fullscreen_bg">@android:color/system_neutral1_900</color> <!-- The color of the navigation bar icons. Need to be in sync with ic_sysbar_* --> <color name="navigation_bar_icon_color">#E5FFFFFF</color> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 793647d2909d..53aebda8a4b4 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2396,4 +2396,7 @@ <string name="clipboard_edit_image_description">Edit copied image</string> <!-- Label for button to send copied content to a nearby device [CHAR LIMIT=NONE] --> <string name="clipboard_send_nearby_description">Send to nearby device</string> + + <!-- Generic "add" string [CHAR LIMIT=NONE] --> + <string name="add">Add</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 590cc9b4eb0a..9448d3f342b9 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -771,6 +771,14 @@ <item name="wallpaperTextColor">@*android:color/primary_text_material_dark</item> </style> + <style name="Theme.UserSwitcherActivity" parent="@android:style/Theme.DeviceDefault.NoActionBar"> + <item name="android:statusBarColor">@color/user_switcher_fullscreen_bg</item> + <item name="android:windowBackground">@color/user_switcher_fullscreen_bg</item> + <item name="android:navigationBarColor">@color/user_switcher_fullscreen_bg</item> + <!-- Setting a placeholder will avoid using the SystemUI icon on the splash screen --> + <item name="android:windowSplashScreenAnimatedIcon">@drawable/ic_blank</item> + </style> + <style name="Theme.CreateUser" parent="@style/Theme.SystemUI"> <item name="android:windowIsTranslucent">true</item> <item name="android:windowBackground">#33000000</item> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index c387260005b4..7bc343e91f8c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -856,10 +856,9 @@ public class KeyguardSecurityContainer extends FrameLayout { } private void setupUserSwitcher() { - String currentUserName = mUserSwitcherController.getCurrentUserName(); - mUserSwitcher.setText(currentUserName); + final UserRecord currentUser = mUserSwitcherController.getCurrentUserRecord(); + mUserSwitcher.setText(mUserSwitcherController.getCurrentUserName()); - final UserRecord currentUser = getCurrentUser(); ViewGroup anchor = mView.findViewById(R.id.user_switcher_anchor); BaseUserAdapter adapter = new BaseUserAdapter(mUserSwitcherController) { @Override @@ -961,16 +960,6 @@ public class KeyguardSecurityContainer extends FrameLayout { }); } - private UserRecord getCurrentUser() { - for (int i = 0; i < mUserSwitcherController.getUsers().size(); ++i) { - UserRecord userRecord = mUserSwitcherController.getUsers().get(i); - if (userRecord.isCurrent) { - return userRecord; - } - } - return null; - } - /** * Each view will get half the width. Yes, it would be easier to use something other than * FrameLayout but it was too disruptive to downstream projects to change. diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java index 1af9e413b226..552f18894408 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java @@ -110,6 +110,8 @@ public class Flags { public static final BooleanFlag NEW_FOOTER = new BooleanFlag(504, false); public static final BooleanFlag NEW_HEADER = new BooleanFlag(505, false); + public static final ResourceBooleanFlag FULL_SCREEN_USER_SWITCHER = + new ResourceBooleanFlag(506, R.bool.config_enableFullscreenUserSwitcher); /***************************************/ // 600- status bar diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java index eb5db299ec69..357a12b09b0d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java @@ -18,13 +18,16 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.DejankUtils.whitelistIpcs; +import android.content.Intent; import android.os.UserManager; import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; +import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.qs.DetailAdapter; import com.android.systemui.qs.FooterActionsView; @@ -32,6 +35,7 @@ import com.android.systemui.qs.QSDetailDisplayer; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.user.UserSwitchDialogController; import com.android.systemui.statusbar.policy.UserSwitcherController; +import com.android.systemui.user.UserSwitcherActivity; import com.android.systemui.util.ViewController; import javax.inject.Inject; @@ -43,6 +47,7 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { private final QSDetailDisplayer mQsDetailDisplayer; private final FalsingManager mFalsingManager; private final UserSwitchDialogController mUserSwitchDialogController; + private final ActivityStarter mActivityStarter; private final FeatureFlags mFeatureFlags; private UserSwitcherController.BaseUserAdapter mUserListener; @@ -54,7 +59,14 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { return; } - if (mFeatureFlags.isEnabled(Flags.NEW_USER_SWITCHER)) { + if (mFeatureFlags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)) { + Intent intent = new Intent(v.getContext(), UserSwitcherActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + + mActivityStarter.startActivity(intent, true /* dismissShade */, + ActivityLaunchAnimator.Controller.fromView(v, null), + true /* showOverlockscreenwhenlocked */); + } else if (mFeatureFlags.isEnabled(Flags.NEW_USER_SWITCHER)) { mUserSwitchDialogController.showDialog(v); } else { View center = mView.getChildCount() > 0 ? mView.getChildAt(0) : mView; @@ -76,31 +88,35 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { private final QSDetailDisplayer mQsDetailDisplayer; private final FalsingManager mFalsingManager; private final UserSwitchDialogController mUserSwitchDialogController; + private final ActivityStarter mActivityStarter; private final FeatureFlags mFeatureFlags; @Inject public Factory(UserManager userManager, UserSwitcherController userSwitcherController, QSDetailDisplayer qsDetailDisplayer, FalsingManager falsingManager, - UserSwitchDialogController userSwitchDialogController, FeatureFlags featureFlags) { + UserSwitchDialogController userSwitchDialogController, FeatureFlags featureFlags, + ActivityStarter activityStarter) { mUserManager = userManager; mUserSwitcherController = userSwitcherController; mQsDetailDisplayer = qsDetailDisplayer; mFalsingManager = falsingManager; mUserSwitchDialogController = userSwitchDialogController; + mActivityStarter = activityStarter; mFeatureFlags = featureFlags; } public MultiUserSwitchController create(FooterActionsView view) { return new MultiUserSwitchController(view.findViewById(R.id.multi_user_switch), mUserManager, mUserSwitcherController, mQsDetailDisplayer, - mFalsingManager, mUserSwitchDialogController, mFeatureFlags); + mFalsingManager, mUserSwitchDialogController, mFeatureFlags, + mActivityStarter); } } private MultiUserSwitchController(MultiUserSwitch view, UserManager userManager, UserSwitcherController userSwitcherController, QSDetailDisplayer qsDetailDisplayer, FalsingManager falsingManager, UserSwitchDialogController userSwitchDialogController, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, ActivityStarter activityStarter) { super(view); mUserManager = userManager; mUserSwitcherController = userSwitcherController; @@ -108,6 +124,7 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { mFalsingManager = falsingManager; mUserSwitchDialogController = userSwitchDialogController; mFeatureFlags = featureFlags; + mActivityStarter = activityStarter; } @Override @@ -166,5 +183,4 @@ public class MultiUserSwitchController extends ViewController<MultiUserSwitch> { return whitelistIpcs(() -> mUserManager.isUserSwitcherEnabled( getResources().getBoolean(R.bool.qs_show_user_switcher_for_single_user))); } - } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java index 1b73595beb7c..3ece240bc576 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java @@ -81,7 +81,6 @@ import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.tiles.UserDetailView; import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower; import com.android.systemui.settings.UserTracker; -import com.android.systemui.statusbar.phone.NotificationShadeWindowView; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.user.CreateUserActivity; @@ -159,7 +158,7 @@ public class UserSwitcherController implements Dumpable { private final AtomicBoolean mGuestIsResetting; private final AtomicBoolean mGuestCreationScheduled; private FalsingManager mFalsingManager; - private NotificationShadeWindowView mRootView; + private View mView; @Inject public UserSwitcherController(Context context, @@ -458,6 +457,19 @@ public class UserSwitcherController implements Dumpable { } } + /** + * @return UserRecord for the current user + */ + public @Nullable UserRecord getCurrentUserRecord() { + for (int i = 0; i < mUsers.size(); ++i) { + UserRecord userRecord = mUsers.get(i); + if (userRecord.isCurrent) { + return userRecord; + } + } + return null; + } + @VisibleForTesting void onUserListItemClicked(UserRecord record, DialogShower dialogShower) { int id; @@ -504,7 +516,7 @@ public class UserSwitcherController implements Dumpable { protected void switchToUserId(int id) { try { mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder - .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mRootView) + .withView(InteractionJankMonitor.CUJ_USER_SWITCH, mView) .setTimeout(MULTI_USER_JOURNEY_TIMEOUT)); mLatencyTracker.onActionStart(LatencyTracker.ACTION_USER_SWITCH); pauseRefreshUsers(); @@ -823,8 +835,11 @@ public class UserSwitcherController implements Dumpable { return guest.id; } - public void init(NotificationShadeWindowView notificationShadeWindowView) { - mRootView = notificationShadeWindowView; + /** + * Require a view for jank detection + */ + public void init(View view) { + mView = view; } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java index 0ad0984e8231..469d54ff8ffa 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java +++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java @@ -16,21 +16,32 @@ package com.android.systemui.user; +import android.app.Activity; + import com.android.settingslib.users.EditUserInfoController; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; /** * Dagger module for User related classes. */ @Module -public class UserModule { +public abstract class UserModule { private static final String FILE_PROVIDER_AUTHORITY = "com.android.systemui.fileprovider"; @Provides - EditUserInfoController provideEditUserInfoController() { + public static EditUserInfoController provideEditUserInfoController() { return new EditUserInfoController(FILE_PROVIDER_AUTHORITY); } + + /** Provides UserSwitcherActivity */ + @Binds + @IntoMap + @ClassKey(UserSwitcherActivity.class) + public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity); } diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt new file mode 100644 index 000000000000..d6a8ab270b84 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2022 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.systemui.user + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.InsetDrawable +import android.graphics.drawable.LayerDrawable +import android.os.Bundle +import android.os.UserManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowInsets.Type +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.ImageView +import android.widget.TextView + +import androidx.constraintlayout.helper.widget.Flow + +import com.android.internal.util.UserIcons +import com.android.settingslib.Utils +import com.android.systemui.R +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.FalsingManager.LOW_PENALTY +import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdapter +import com.android.systemui.statusbar.policy.UserSwitcherController.UserRecord +import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA +import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA +import com.android.systemui.util.LifecycleActivity + +import javax.inject.Inject + +private const val USER_VIEW = "user_view" + +/** + * Support a fullscreen user switcher + */ +class UserSwitcherActivity @Inject constructor( + private val userSwitcherController: UserSwitcherController, + private val broadcastDispatcher: BroadcastDispatcher, + private val layoutInflater: LayoutInflater, + private val falsingManager: FalsingManager, + private val userManager: UserManager +) : LifecycleActivity() { + + private lateinit var parent: ViewGroup + private lateinit var broadcastReceiver: BroadcastReceiver + private var popupMenu: UserSwitcherPopupMenu? = null + private lateinit var addButton: View + private var addUserItem: UserRecord? = null + private var addGuestItem: UserRecord? = null + + private val adapter = object : BaseUserAdapter(userSwitcherController) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val item = getItem(position) + var view = convertView as ViewGroup? + if (view == null) { + view = layoutInflater.inflate( + R.layout.user_switcher_fullscreen_item, + parent, + false + ) as ViewGroup + } + (view.getChildAt(0) as ImageView).apply { + setImageDrawable(getDrawable(item)) + } + (view.getChildAt(1) as TextView).apply { + setText(getName(getContext(), item)) + } + + view.setEnabled(item.isSwitchToEnabled) + view.setAlpha( + if (view.isEnabled()) { + USER_SWITCH_ENABLED_ALPHA + } else { + USER_SWITCH_DISABLED_ALPHA + } + ) + view.setTag(USER_VIEW) + return view + } + + fun findUserIcon(item: UserRecord): Drawable { + if (item.info == null) { + return getIconDrawable(this@UserSwitcherActivity, item) + } + val userIcon = userManager.getUserIcon(item.info.id) + if (userIcon != null) { + return BitmapDrawable(userIcon) + } + return UserIcons.getDefaultUserIcon(resources, item.info.id, false) + } + + private fun getDrawable(item: UserRecord): Drawable { + var drawable = if (item.isCurrent && item.isGuest) { + getDrawable(R.drawable.ic_avatar_guest_user) + } else { + findUserIcon(item) + } + drawable.mutate() + + if (!item.isCurrent && !item.isSwitchToEnabled) { + drawable.setTint( + resources.getColor( + R.color.kg_user_switcher_restricted_avatar_icon_color, + getTheme() + ) + ) + } + + val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate() + as LayerDrawable + if (item == userSwitcherController.getCurrentUserRecord()) { + (ld.getDrawable(1) as GradientDrawable).apply { + val stroke = resources + .getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width) + val color = Utils.getColorAttrDefaultColor( + this@UserSwitcherActivity, + com.android.internal.R.attr.colorAccentPrimary + ) + + setStroke(stroke, color) + } + } + + ld.addLayer( + InsetDrawable( + drawable, + resources.getDimensionPixelSize( + R.dimen.user_switcher_icon_large_margin + ) + ) + ) + + return ld + } + + override fun notifyDataSetChanged() { + super.notifyDataSetChanged() + buildUserViews() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.user_switcher_fullscreen) + + parent = requireViewById<ViewGroup>(R.id.user_switcher_root).apply { + setOnApplyWindowInsetsListener { + v: View, insets: WindowInsets -> + v.apply { + val l = getPaddingLeft() + val t = getPaddingTop() + val r = getPaddingRight() + setPadding(l, t, r, insets.getInsets(Type.systemBars()).bottom) + } + + WindowInsets.CONSUMED + } + } + + requireViewById<View>(R.id.cancel).apply { + setOnClickListener { + _ -> finish() + } + } + + addButton = requireViewById<View>(R.id.add).apply { + setOnClickListener { + _ -> showPopupMenu() + } + } + + userSwitcherController.init(parent) + initBroadcastReceiver() + buildUserViews() + } + + private fun showPopupMenu() { + val items = mutableListOf<UserRecord>() + addUserItem?.let { items.add(it) } + addGuestItem?.let { items.add(it) } + + var popupMenuAdapter = ItemAdapter( + this, + R.layout.user_switcher_fullscreen_popup_item, + layoutInflater, + { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item) }, + { item: UserRecord -> adapter.findUserIcon(item) } + ) + popupMenuAdapter.addAll(items) + + popupMenu = UserSwitcherPopupMenu(this, falsingManager).apply { + setAnchorView(addButton) + setAdapter(popupMenuAdapter) + setOnItemClickListener { + parent: AdapterView<*>, view: View, pos: Int, id: Long -> + if (falsingManager.isFalseTap(LOW_PENALTY) || !view.isEnabled()) { + return@setOnItemClickListener + } + // -1 for the header + val item = popupMenuAdapter.getItem(pos - 1) + adapter.onUserListItemClicked(item) + + dismiss() + popupMenu = null + } + + show() + } + } + + private fun buildUserViews() { + var count = 0 + var start = 0 + for (i in 0 until parent.getChildCount()) { + if (parent.getChildAt(i).getTag() == USER_VIEW) { + if (count == 0) start = i + count++ + } + } + parent.removeViews(start, count) + + val flow = requireViewById<Flow>(R.id.flow) + for (i in 0 until adapter.getCount()) { + val item = adapter.getItem(i) + if (item.isAddUser) { + addUserItem = item + } else if (item.isGuest && item.info == null) { + addGuestItem = item + } else { + val userView = adapter.getView(i, null, parent) + userView.setId(View.generateViewId()) + parent.addView(userView) + + // Views must have an id and a parent in order for Flow to lay them out + flow.addView(userView) + + userView.setOnClickListener { v -> + if (falsingManager.isFalseTap(LOW_PENALTY) || !v.isEnabled()) { + return@setOnClickListener + } + + if (!item.isCurrent || item.isGuest) { + adapter.onUserListItemClicked(item) + } + } + } + } + + if (addUserItem != null || addGuestItem != null) { + addButton.visibility = View.VISIBLE + } else { + addButton.visibility = View.GONE + } + } + + override fun onBackPressed() { + finish() + } + + override fun onDestroy() { + super.onDestroy() + + broadcastDispatcher.unregisterReceiver(broadcastReceiver) + } + + private fun initBroadcastReceiver() { + broadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.getAction() + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + finish() + } + } + } + + val filter = IntentFilter() + filter.addAction(Intent.ACTION_SCREEN_OFF) + broadcastDispatcher.registerReceiver(broadcastReceiver, filter) + } + + private class ItemAdapter( + val parentContext: Context, + val resource: Int, + val layoutInflater: LayoutInflater, + val textGetter: (UserRecord) -> String, + val iconGetter: (UserRecord) -> Drawable + ) : ArrayAdapter<UserRecord>(parentContext, resource) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val item = getItem(position) + val view = convertView ?: layoutInflater.inflate(resource, parent, false) + + view.requireViewById<ImageView>(R.id.icon).apply { + setImageDrawable(iconGetter(item)) + } + view.requireViewById<TextView>(R.id.text).apply { + setText(textGetter(item)) + } + + return view + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt new file mode 100644 index 000000000000..896354737e46 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherPopupMenu.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 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.systemui.user + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.ShapeDrawable +import android.view.View +import android.view.View.MeasureSpec +import android.widget.ListAdapter +import android.widget.ListPopupWindow +import android.widget.ListView + +import com.android.systemui.R +import com.android.systemui.plugins.FalsingManager + +/** + * Popup menu for displaying items on the fullscreen user switcher. + */ +class UserSwitcherPopupMenu( + private val context: Context, + private val falsingManager: FalsingManager +) : ListPopupWindow(context) { + + private val res = context.resources + private var adapter: ListAdapter? = null + + init { + setBackgroundDrawable( + res.getDrawable(R.drawable.bouncer_user_switcher_popup_bg, context.getTheme()) + ) + setModal(true) + setOverlapAnchor(true) + } + + override fun setAdapter(adapter: ListAdapter?) { + super.setAdapter(adapter) + this.adapter = adapter + } + + /** + * Show the dialog. + */ + override fun show() { + // need to call show() first in order to construct the listView + super.show() + val listView = getListView() + + listView.setVerticalScrollBarEnabled(false) + listView.setHorizontalScrollBarEnabled(false) + + // Creates a transparent spacer between items + val shape = ShapeDrawable() + shape.setAlpha(0) + listView.setDivider(shape) + listView.setDividerHeight(res.getDimensionPixelSize( + R.dimen.bouncer_user_switcher_popup_divider_height)) + + val height = res.getDimensionPixelSize(R.dimen.bouncer_user_switcher_popup_header_height) + listView.addHeaderView(createSpacer(height), null, false) + listView.addFooterView(createSpacer(height), null, false) + setWidth(findMaxWidth(listView)) + + super.show() + } + + private fun findMaxWidth(listView: ListView): Int { + var maxWidth = 0 + adapter?.let { + val parentWidth = res.getDisplayMetrics().widthPixels + val spec = MeasureSpec.makeMeasureSpec( + (parentWidth * 0.25).toInt(), + MeasureSpec.AT_MOST + ) + + for (i in 0 until it.getCount()) { + val child = it.getView(i, null, listView) + child.measure(spec, MeasureSpec.UNSPECIFIED) + maxWidth = Math.max(child.getMeasuredWidth(), maxWidth) + } + } + return maxWidth + } + + private fun createSpacer(height: Int): View { + return object : View(context) { + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + setMeasuredDimension(1, height) + } + + override fun draw(canvas: Canvas) { + } + } + } +} |