Merge "Fullscreen user switcher"

This commit is contained in:
TreeHugger Robot
2022-02-08 23:11:31 +00:00
committed by Android (Google) Code Review
17 changed files with 770 additions and 26 deletions

View File

@@ -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>

View 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

View File

@@ -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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -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

View File

@@ -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)));
}
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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
}
}
}

View File

@@ -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) {
}
}
}
}