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
This commit is contained in:
@@ -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>
|
||||
|
||||
45
packages/SystemUI/docs/user-switching.md
Normal file
45
packages/SystemUI/docs/user-switching.md
Normal file
@@ -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
|
||||
@@ -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>
|
||||
|
||||
46
packages/SystemUI/res/drawable/user_switcher_icon_large.xml
Normal file
46
packages/SystemUI/res/drawable/user_switcher_icon_large.xml
Normal file
@@ -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>
|
||||
70
packages/SystemUI/res/layout/user_switcher_fullscreen.xml
Normal file
70
packages/SystemUI/res/layout/user_switcher_fullscreen.xml
Normal file
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user