From 9a1928ea36057d0824d20ccc797ea6d55bcaee3b Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 2 Aug 2018 12:59:55 -0700 Subject: [PATCH] Move profile selector dialog to Settings Bug: 77600770 Test: robotest Change-Id: I601dabfd925a1990b7bd9115ef579bff9039a7c0 --- .../settings/dashboard/CategoryManager.java | 11 -- .../settings/dashboard/DashboardAdapter.java | 10 +- .../dashboard/DashboardFeatureProviderImpl.java | 4 +- .../settings/dashboard/DashboardFragment.java | 4 +- .../profileselector/ProfileSelectDialog.java | 93 +++++++++ .../dashboard/profileselector/UserAdapter.java | 214 +++++++++++++++++++++ .../settings/print/PrintSettingsFragment.java | 13 +- .../ProfileSettingsPreferenceFragment.java | 8 +- .../settings/dashboard/DashboardDataTest.java | 9 +- .../profileselector/ProfileSelectDialogTest.java | 90 +++++++++ 10 files changed, 420 insertions(+), 36 deletions(-) create mode 100644 src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java create mode 100644 src/com/android/settings/dashboard/profileselector/UserAdapter.java rename src/com/android/settings/{utils => print}/ProfileSettingsPreferenceFragment.java (92%) create mode 100644 tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java diff --git a/src/com/android/settings/dashboard/CategoryManager.java b/src/com/android/settings/dashboard/CategoryManager.java index 407207226f..2d830def20 100644 --- a/src/com/android/settings/dashboard/CategoryManager.java +++ b/src/com/android/settings/dashboard/CategoryManager.java @@ -216,15 +216,4 @@ public class CategoryManager { } } } - - /** - * Sort priority value for tiles within a single {@code DashboardCategory}. - * - * @see #sortCategories(Context, Map) - */ - private synchronized void sortCategoriesForExternalTiles(Context context, - DashboardCategory dashboardCategory) { - dashboardCategory.sortTiles(context.getPackageName()); - - } } diff --git a/src/com/android/settings/dashboard/DashboardAdapter.java b/src/com/android/settings/dashboard/DashboardAdapter.java index b278f60530..d256a5d8b7 100644 --- a/src/com/android/settings/dashboard/DashboardAdapter.java +++ b/src/com/android/settings/dashboard/DashboardAdapter.java @@ -31,6 +31,11 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.R.id; @@ -51,11 +56,6 @@ import com.android.settingslib.utils.IconCache; import java.util.List; -import androidx.annotation.VisibleForTesting; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - public class DashboardAdapter extends RecyclerView.Adapter implements SummaryLoader.SummaryConsumer, SuggestionAdapter.Callback, LifecycleObserver, OnSaveInstanceState { diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java index a3410bd46a..1bef4151e2 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java @@ -40,11 +40,11 @@ import androidx.preference.Preference; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsActivity; +import com.android.settings.dashboard.profileselector.ProfileSelectDialog; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin; import com.android.settingslib.drawer.DashboardCategory; -import com.android.settingslib.drawer.ProfileSelectDialog; import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.TileUtils; import com.android.settingslib.utils.ThreadUtils; @@ -92,7 +92,7 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } final List tiles = category.getTiles(); if (tiles == null || tiles.isEmpty()) { - Log.d(TAG, "tile list is empty, skipping category " + category.title); + Log.d(TAG, "tile list is empty, skipping category " + category.key); return null; } final List preferences = new ArrayList<>(); diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index fceec3b91b..b24164f5b4 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -343,7 +343,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } final List tiles = category.getTiles(); if (tiles == null) { - Log.d(TAG, "tile list is empty, skipping category " + category.title); + Log.d(TAG, "tile list is empty, skipping category " + category.key); return; } // Create a list to track which tiles are to be removed. @@ -356,7 +356,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment final Context context = getContext(); mSummaryLoader = new SummaryLoader(getActivity(), getCategoryKey()); mSummaryLoader.setSummaryConsumer(this); - final TypedArray a = context.obtainStyledAttributes(new int[] { + final TypedArray a = context.obtainStyledAttributes(new int[]{ android.R.attr.colorControlNormal}); final int tintColor = a.getColor(0, context.getColor(android.R.color.white)); a.recycle(); diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java new file mode 100644 index 0000000000..e56c58b190 --- /dev/null +++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2015 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.settings.dashboard.profileselector; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.FragmentManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Log; + +import com.android.settingslib.drawer.Tile; + +import java.util.List; + +public class ProfileSelectDialog extends DialogFragment implements OnClickListener { + + private static final String TAG = "ProfileSelectDialog"; + private static final String ARG_SELECTED_TILE = "selectedTile"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private Tile mSelectedTile; + + public static void show(FragmentManager manager, Tile tile) { + ProfileSelectDialog dialog = new ProfileSelectDialog(); + Bundle args = new Bundle(); + args.putParcelable(ARG_SELECTED_TILE, tile); + dialog.setArguments(args); + dialog.show(manager, "select_profile"); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mSelectedTile = getArguments().getParcelable(ARG_SELECTED_TILE); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Context context = getActivity(); + AlertDialog.Builder builder = new AlertDialog.Builder(context); + UserAdapter adapter = UserAdapter.createUserAdapter(UserManager.get(context), context, + mSelectedTile.userHandle); + builder.setTitle(com.android.settingslib.R.string.choose_profile) + .setAdapter(adapter, this); + + return builder.create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + UserHandle user = mSelectedTile.userHandle.get(which); + // Show menu on top level items. + mSelectedTile.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + getActivity().startActivityAsUser(mSelectedTile.intent, user); + } + + public static void updateUserHandlesIfNeeded(Context context, Tile tile) { + List userHandles = tile.userHandle; + if (tile.userHandle == null || tile.userHandle.size() <= 1) { + return; + } + final UserManager userManager = UserManager.get(context); + for (int i = userHandles.size() - 1; i >= 0; i--) { + if (userManager.getUserInfo(userHandles.get(i).getIdentifier()) == null) { + if (DEBUG) { + Log.d(TAG, "Delete the user: " + userHandles.get(i).getIdentifier()); + } + userHandles.remove(i); + } + } + } +} diff --git a/src/com/android/settings/dashboard/profileselector/UserAdapter.java b/src/com/android/settings/dashboard/profileselector/UserAdapter.java new file mode 100644 index 0000000000..46c87a1600 --- /dev/null +++ b/src/com/android/settings/dashboard/profileselector/UserAdapter.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2014 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.settings.dashboard.profileselector; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.UserInfo; +import android.database.DataSetObserver; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.os.UserManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.SpinnerAdapter; +import android.widget.TextView; + +import com.android.internal.util.UserIcons; +import com.android.settingslib.R; +import com.android.settingslib.drawable.UserIconDrawable; + +import java.util.ArrayList; +import java.util.List; + +/** + * Adapter for a spinner that shows a list of users. + */ +public class UserAdapter implements SpinnerAdapter, ListAdapter { + /** Holder for user details */ + public static class UserDetails { + private final UserHandle mUserHandle; + private final String mName; + private final Drawable mIcon; + + public UserDetails(UserHandle userHandle, UserManager um, Context context) { + mUserHandle = userHandle; + UserInfo userInfo = um.getUserInfo(mUserHandle.getIdentifier()); + Drawable icon; + if (userInfo.isManagedProfile()) { + mName = context.getString(R.string.managed_user_title); + icon = context.getDrawable( + com.android.internal.R.drawable.ic_corp_badge); + } else { + mName = userInfo.name; + final int userId = userInfo.id; + if (um.getUserIcon(userId) != null) { + icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId)); + } else { + icon = UserIcons.getDefaultUserIcon( + context.getResources(), userId, /* light= */ false); + } + } + this.mIcon = encircle(context, icon); + } + + private static Drawable encircle(Context context, Drawable icon) { + return new UserIconDrawable(UserIconDrawable.getSizeForList(context)) + .setIconDrawable(icon).bake(); + } + } + + private ArrayList data; + private final LayoutInflater mInflater; + + public UserAdapter(Context context, ArrayList users) { + if (users == null) { + throw new IllegalArgumentException("A list of user details must be provided"); + } + this.data = users; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + public UserHandle getUserHandle(int position) { + if (position < 0 || position >= data.size()) { + return null; + } + return data.get(position).mUserHandle; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + final View row = convertView != null ? convertView : createUser(parent); + + UserDetails user = data.get(position); + ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(user.mIcon); + ((TextView) row.findViewById(android.R.id.title)).setText(getTitle(user)); + return row; + } + + private int getTitle(UserDetails user) { + int userHandle = user.mUserHandle.getIdentifier(); + if (userHandle == UserHandle.USER_CURRENT + || userHandle == ActivityManager.getCurrentUser()) { + return R.string.category_personal; + } else { + return R.string.category_work; + } + } + + private View createUser(ViewGroup parent) { + return mInflater.inflate(R.layout.user_preference, parent, false); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + // We don't support observers + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public UserAdapter.UserDetails getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return data.get(position).mUserHandle.getIdentifier(); + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + return getDropDownView(position, convertView, parent); + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean isEmpty() { + return data.isEmpty(); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + /** + * Creates a {@link UserAdapter} if there is more than one + * profile on the device. + * + *

The adapter can be used to populate a spinner that switches between the Settings + * app on the different profiles. + * + * @return a {@link UserAdapter} or null if there is only one + * profile. + */ + public static UserAdapter createUserSpinnerAdapter(UserManager userManager, Context context) { + List userProfiles = userManager.getUserProfiles(); + if (userProfiles.size() < 2) { + return null; + } + + UserHandle myUserHandle = new UserHandle(UserHandle.myUserId()); + // The first option should be the current profile + userProfiles.remove(myUserHandle); + userProfiles.add(0, myUserHandle); + + return createUserAdapter(userManager, context, userProfiles); + } + + public static UserAdapter createUserAdapter( + UserManager userManager, Context context, List userProfiles) { + ArrayList userDetails = new ArrayList<>(userProfiles.size()); + final int count = userProfiles.size(); + for (int i = 0; i < count; i++) { + userDetails.add(new UserDetails(userProfiles.get(i), userManager, context)); + } + return new UserAdapter(context, userDetails); + } +} diff --git a/src/com/android/settings/print/PrintSettingsFragment.java b/src/com/android/settings/print/PrintSettingsFragment.java index 899acc7094..2f1f63dcb5 100644 --- a/src/com/android/settings/print/PrintSettingsFragment.java +++ b/src/com/android/settings/print/PrintSettingsFragment.java @@ -45,11 +45,16 @@ import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; +import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.AsyncTaskLoader; +import androidx.loader.content.Loader; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; + import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Indexable; -import com.android.settings.utils.ProfileSettingsPreferenceFragment; import com.android.settings.widget.AppPreference; import com.android.settingslib.search.SearchIndexable; @@ -57,12 +62,6 @@ import java.text.DateFormat; import java.util.ArrayList; import java.util.List; -import androidx.loader.app.LoaderManager.LoaderCallbacks; -import androidx.loader.content.AsyncTaskLoader; -import androidx.loader.content.Loader; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; - /** * Fragment with the top level print settings. */ diff --git a/src/com/android/settings/utils/ProfileSettingsPreferenceFragment.java b/src/com/android/settings/print/ProfileSettingsPreferenceFragment.java similarity index 92% rename from src/com/android/settings/utils/ProfileSettingsPreferenceFragment.java rename to src/com/android/settings/print/ProfileSettingsPreferenceFragment.java index e1c4d285db..b616ccc432 100644 --- a/src/com/android/settings/utils/ProfileSettingsPreferenceFragment.java +++ b/src/com/android/settings/print/ProfileSettingsPreferenceFragment.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.settings.utils; +package com.android.settings.print; import android.content.Context; import android.content.Intent; @@ -27,7 +27,7 @@ import android.widget.Spinner; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; -import com.android.settingslib.drawer.UserAdapter; +import com.android.settings.dashboard.profileselector.UserAdapter; /** * Base fragment class for per profile settings. @@ -46,8 +46,8 @@ public abstract class ProfileSettingsPreferenceFragment extends SettingsPreferen spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, - long id) { - UserHandle selectedUser = profileSpinnerAdapter.getUserHandle(position); + long id) { + final UserHandle selectedUser = profileSpinnerAdapter.getUserHandle(position); if (selectedUser.getIdentifier() != UserHandle.myUserId()) { Intent intent = new Intent(getIntentActionString()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java index 3d7eb13bdc..5ea7d2d11c 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java @@ -29,6 +29,10 @@ import static org.mockito.Mockito.when; import android.app.PendingIntent; import android.service.settings.suggestions.Suggestion; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListUpdateCallback; + import com.android.settings.dashboard.conditional.AirplaneModeCondition; import com.android.settings.dashboard.conditional.Condition; import com.android.settings.testutils.SettingsRobolectricTestRunner; @@ -46,10 +50,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListUpdateCallback; - @RunWith(SettingsRobolectricTestRunner.class) public class DashboardDataTest { @@ -95,7 +95,6 @@ public class DashboardDataTest { // Build category mTestCategoryTile.title = TEST_CATEGORY_TILE_TITLE; - mDashboardCategory.title = "test"; mDashboardCategory.addTile(mTestCategoryTile); diff --git a/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java new file mode 100644 index 0000000000..70bac9a7bf --- /dev/null +++ b/tests/robotests/src/com/android/settings/dashboard/profileselector/ProfileSelectDialogTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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.settings.dashboard.profileselector; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; + +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.drawer.Tile; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(SettingsRobolectricTestRunner.class) +public class ProfileSelectDialogTest { + + private static final UserHandle NORMAL_USER = UserHandle.of(1111); + private static final UserHandle REMOVED_USER = UserHandle.of(2222); + + @Mock + private Context mContext; + @Mock + private UserManager mUserManager; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getSystemService(Context.USER_SERVICE)).thenReturn(mUserManager); + final UserInfo userInfo = new UserInfo( + NORMAL_USER.getIdentifier(), "test_user", UserInfo.FLAG_RESTRICTED); + when(mUserManager.getUserInfo(NORMAL_USER.getIdentifier())).thenReturn(userInfo); + } + + @Test + public void updateUserHandlesIfNeeded_Normal() { + final Tile tile = new Tile(new ActivityInfo()); + tile.intent = new Intent(); + tile.userHandle.add(NORMAL_USER); + + ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile); + + assertThat(tile.userHandle).hasSize(1); + assertThat(tile.userHandle.get(0).getIdentifier()).isEqualTo(NORMAL_USER.getIdentifier()); + verify(mUserManager, never()).getUserInfo(NORMAL_USER.getIdentifier()); + } + + @Test + public void updateUserHandlesIfNeeded_Remove() { + final Tile tile = new Tile(new ActivityInfo()); + tile.intent = new Intent(); + tile.userHandle.add(REMOVED_USER); + tile.userHandle.add(NORMAL_USER); + tile.userHandle.add(REMOVED_USER); + + ProfileSelectDialog.updateUserHandlesIfNeeded(mContext, tile); + + assertThat(tile.userHandle).hasSize(1); + assertThat(tile.userHandle.get(0).getIdentifier()).isEqualTo(NORMAL_USER.getIdentifier()); + verify(mUserManager, times(1)).getUserInfo(NORMAL_USER.getIdentifier()); + verify(mUserManager, times(2)).getUserInfo(REMOVED_USER.getIdentifier()); + } +} -- 2.11.0