diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 195c44ecd23..b7280633ff1 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4403,9 +4403,12 @@ - + android:exported="true"> + + + + + - - - - - - - - diff --git a/src/com/android/settings/applications/credentials/CombinedProviderInfo.java b/src/com/android/settings/applications/credentials/CombinedProviderInfo.java index f8a3b0fc4a6..4f278709f6b 100644 --- a/src/com/android/settings/applications/credentials/CombinedProviderInfo.java +++ b/src/com/android/settings/applications/credentials/CombinedProviderInfo.java @@ -16,6 +16,7 @@ package com.android.settings.applications.credentials; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -24,7 +25,6 @@ import android.content.pm.ServiceInfo; import android.credentials.CredentialProviderInfo; import android.graphics.drawable.Drawable; import android.os.UserHandle; -import android.os.UserManager; import android.service.autofill.AutofillServiceInfo; import android.text.TextUtils; import android.util.IconDrawableFactory; @@ -49,7 +49,7 @@ public final class CombinedProviderInfo { private static final String TAG = "CombinedProviderInfo"; private static final String SETTINGS_ACTIVITY_INTENT_ACTION = "android.intent.action.MAIN"; private static final String SETTINGS_ACTIVITY_INTENT_CATEGORY = - "android.intent.category.LAUNCHER"; + "android.intent.category.DEFAULT"; private final List mCredentialProviderInfos; private final @Nullable AutofillServiceInfo mAutofillServiceInfo; @@ -327,10 +327,8 @@ public final class CombinedProviderInfo { } public static @Nullable Intent createSettingsActivityIntent( - @NonNull Context context, @Nullable CharSequence packageName, - @Nullable CharSequence settingsActivity, - int currentUserId) { + @Nullable CharSequence settingsActivity) { if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(settingsActivity)) { return null; } @@ -350,19 +348,25 @@ public final class CombinedProviderInfo { Intent intent = new Intent(SETTINGS_ACTIVITY_INTENT_ACTION); intent.addCategory(SETTINGS_ACTIVITY_INTENT_CATEGORY); intent.setComponent(cn); - - int contextUserId = context.getUser().getIdentifier(); - if (currentUserId != contextUserId && UserManager.isHeadlessSystemUserMode()) { - Log.w( - TAG, - "onLeftSideClicked(): using context for current user (" - + currentUserId - + ") instead of user " - + contextUserId - + " on headless system user mode"); - context = context.createContextAsUser(UserHandle.of(currentUserId), /* flags= */ 0); - } - + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); return intent; } + + /** Launches the settings activity intent. */ + public static void launchSettingsActivityIntent( + @NonNull Context context, + @Nullable CharSequence packageName, + @Nullable CharSequence settingsActivity, + int userId) { + Intent settingsIntent = createSettingsActivityIntent(packageName, settingsActivity); + if (settingsIntent == null) { + return; + } + + try { + context.startActivityAsUser(settingsIntent, UserHandle.of(userId)); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "Failed to open settings activity", e); + } + } } diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java index 2f04b62a821..8ea7a9c1697 100644 --- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java +++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java @@ -20,7 +20,6 @@ import static androidx.lifecycle.Lifecycle.Event.ON_CREATE; import android.app.Activity; import android.app.Dialog; -import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -653,16 +652,8 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl @Override public void onLeftSideClicked() { - Intent settingsIntent = - CombinedProviderInfo.createSettingsActivityIntent( - mContext, packageName, settingsActivity, getUser()); - if (settingsIntent != null) { - try { - mContext.startActivity(settingsIntent); - } catch (ActivityNotFoundException e) { - Log.e(TAG, "Failed to open settings activity", e); - } - } + CombinedProviderInfo.launchSettingsActivityIntent( + mContext, packageName, settingsActivity, getUser()); } }); diff --git a/src/com/android/settings/applications/credentials/CredentialsPickerActivity.java b/src/com/android/settings/applications/credentials/CredentialsPickerActivity.java index 495c104d661..479a184752f 100644 --- a/src/com/android/settings/applications/credentials/CredentialsPickerActivity.java +++ b/src/com/android/settings/applications/credentials/CredentialsPickerActivity.java @@ -16,15 +16,53 @@ package com.android.settings.applications.credentials; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.settings.SettingsActivity; -/** Standalone activity used to launch a {@link DefaultCombinedPicker} fragment. */ +/** + * Standalone activity used to launch a {@link DefaultCombinedPicker} fragment if the user is a + * normal user, a {@link DefaultCombinedPickerWork} fragment if the user is a work profile or {@link + * DefaultCombinedPickerPrivate} fragment if the user is a private profile. + */ public class CredentialsPickerActivity extends SettingsActivity { + private static final String TAG = "CredentialsPickerActivity"; + + /** Injects the fragment name into the intent so the correct fragment is opened. */ + @VisibleForTesting + public static void injectFragmentIntoIntent(Context context, Intent intent) { + final int userId = UserHandle.myUserId(); + final UserManager userManager = UserManager.get(context); + + if (DefaultCombinedPickerWork.isUserHandledByFragment(userManager, userId)) { + Slog.d(TAG, "Creating picker fragment using work profile"); + intent.putExtra(EXTRA_SHOW_FRAGMENT, DefaultCombinedPickerWork.class.getName()); + } else if (DefaultCombinedPickerPrivate.isUserHandledByFragment(userManager)) { + Slog.d(TAG, "Creating picker fragment using private profile"); + intent.putExtra(EXTRA_SHOW_FRAGMENT, DefaultCombinedPickerPrivate.class.getName()); + } else { + Slog.d(TAG, "Creating picker fragment using normal profile"); + intent.putExtra(EXTRA_SHOW_FRAGMENT, DefaultCombinedPicker.class.getName()); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + injectFragmentIntoIntent(this, getIntent()); + super.onCreate(savedInstanceState); + } @Override protected boolean isValidFragment(String fragmentName) { return super.isValidFragment(fragmentName) - || DefaultCombinedPicker.class.getName().equals(fragmentName); + || DefaultCombinedPicker.class.getName().equals(fragmentName) + || DefaultCombinedPickerWork.class.getName().equals(fragmentName) + || DefaultCombinedPickerPrivate.class.getName().equals(fragmentName); } } diff --git a/src/com/android/settings/applications/credentials/DefaultCombinedPickerPrivate.java b/src/com/android/settings/applications/credentials/DefaultCombinedPickerPrivate.java index 722cb1a1343..8d8af0e385d 100644 --- a/src/com/android/settings/applications/credentials/DefaultCombinedPickerPrivate.java +++ b/src/com/android/settings/applications/credentials/DefaultCombinedPickerPrivate.java @@ -17,14 +17,29 @@ package com.android.settings.applications.credentials; import android.os.UserManager; +import android.util.Slog; import com.android.settings.Utils; import com.android.settings.dashboard.profileselector.ProfileSelectFragment.ProfileType; public class DefaultCombinedPickerPrivate extends DefaultCombinedPicker { + private static final String TAG = "DefaultCombinedPickerPrivate"; + @Override protected int getUser() { UserManager userManager = getContext().getSystemService(UserManager.class); return Utils.getCurrentUserIdOfType(userManager, ProfileType.PRIVATE); } + + /** Returns whether the user is handled by this fragment. */ + public static boolean isUserHandledByFragment(UserManager userManager) { + try { + // If there is no private profile then this will throw an exception. + Utils.getCurrentUserIdOfType(userManager, ProfileType.PRIVATE); + return true; + } catch (IllegalStateException e) { + Slog.e(TAG, "Failed to get private profile user id", e); + return false; + } + } } diff --git a/src/com/android/settings/applications/credentials/DefaultCombinedPickerWork.java b/src/com/android/settings/applications/credentials/DefaultCombinedPickerWork.java index 9808502b63e..945d6b85254 100644 --- a/src/com/android/settings/applications/credentials/DefaultCombinedPickerWork.java +++ b/src/com/android/settings/applications/credentials/DefaultCombinedPickerWork.java @@ -19,13 +19,16 @@ package com.android.settings.applications.credentials; import android.os.UserHandle; import android.os.UserManager; -import com.android.settings.Utils; - public class DefaultCombinedPickerWork extends DefaultCombinedPicker { + private static final String TAG = "DefaultCombinedPickerWork"; @Override protected int getUser() { - UserHandle workProfile = Utils.getManagedProfile(UserManager.get(getContext())); - return workProfile.getIdentifier(); + return UserHandle.myUserId(); + } + + /** Returns whether the user is handled by this fragment. */ + public static boolean isUserHandledByFragment(UserManager userManager, int userId) { + return userManager.isManagedProfile(userId); } } diff --git a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java index 0fb1769063c..49dd7cde221 100644 --- a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java +++ b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java @@ -16,7 +16,6 @@ package com.android.settings.applications.credentials; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.credentials.CredentialManager; @@ -26,11 +25,11 @@ import android.os.UserHandle; import android.provider.Settings; import android.service.autofill.AutofillService; import android.service.autofill.AutofillServiceInfo; +import android.text.TextUtils; import android.view.autofill.AutofillManager; -import android.util.Slog; -import androidx.annotation.Nullable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.preference.Preference; import com.android.internal.annotations.VisibleForTesting; @@ -83,7 +82,7 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon // hand side presses to align the UX. if (PrimaryProviderPreference.shouldUseNewSettingsUi()) { // We need to return an empty intent here since the class we inherit - // from will throw an NPE if we return null and we don't want it to + // from will throw an NPE if we return null and we don't want it to // open anything since we added the buttons. return new Intent(); } @@ -99,10 +98,10 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon topProvider.getAppName(mContext), topProvider.getSettingsSubtitle(), topProvider.getAppIcon(mContext, getUser()), - createSettingsActivityIntent( - topProvider.getPackageName(), topProvider.getSettingsActivity())); + topProvider.getPackageName(), + topProvider.getSettingsActivity()); } else { - updatePreferenceForProvider(preference, null, null, null, null); + updatePreferenceForProvider(preference, null, null, null, null, null); } } @@ -112,7 +111,8 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon @Nullable CharSequence appName, @Nullable String appSubtitle, @Nullable Drawable appIcon, - @Nullable Intent settingsActivityIntent) { + @Nullable CharSequence packageName, + @Nullable CharSequence settingsActivity) { if (appName == null) { preference.setTitle(R.string.app_list_preference_none); } else { @@ -133,13 +133,8 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon primaryPref.setDelegate( new PrimaryProviderPreference.Delegate() { public void onOpenButtonClicked() { - if (settingsActivityIntent != null) { - try { - startActivity(settingsActivityIntent); - } catch (ActivityNotFoundException e) { - Slog.e(TAG, "Failed to open settings activity", e); - } - } + CombinedProviderInfo.launchSettingsActivityIntent( + mContext, packageName, settingsActivity, getUser()); } public void onChangeButtonClicked() { @@ -148,7 +143,7 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon }); // Hide the open button if there is no defined settings activity. - primaryPref.setOpenButtonVisible(settingsActivityIntent != null); + primaryPref.setOpenButtonVisible(!TextUtils.isEmpty(settingsActivity)); primaryPref.setButtonsVisible(appName != null); } } @@ -198,13 +193,8 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon /** Creates an intent to open the credential picker. */ private Intent createIntentToOpenPicker() { - return new Intent(mContext, CredentialsPickerActivity.class); - } - - /** Creates an intent to open the settings activity of the primary provider (if available). */ - public @Nullable Intent createSettingsActivityIntent( - @Nullable String packageName, @Nullable String settingsActivity) { - return CombinedProviderInfo.createSettingsActivityIntent( - mContext, packageName, settingsActivity, getUser()); + final Context context = + mContext.createContextAsUser(UserHandle.of(getUser()), /* flags= */ 0); + return new Intent(context, CredentialsPickerActivity.class); } } diff --git a/tests/unit/src/com/android/settings/applications/credentials/CredentialsPickerActivityTest.java b/tests/unit/src/com/android/settings/applications/credentials/CredentialsPickerActivityTest.java new file mode 100644 index 00000000000..044c23dbc51 --- /dev/null +++ b/tests/unit/src/com/android/settings/applications/credentials/CredentialsPickerActivityTest.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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.applications.credentials; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.UserInfo; +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.common.collect.Lists; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class CredentialsPickerActivityTest { + + @Mock private UserManager mUserManager; + + private Context mMockContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mMockContext = spy(ApplicationProvider.getApplicationContext()); + when(mMockContext.getSystemService(UserManager.class)).thenReturn(mUserManager); + } + + @Test + public void testInjectFragmentIntoIntent_normalProfile() { + Intent intent = new Intent(); + CredentialsPickerActivity.injectFragmentIntoIntent(mMockContext, intent); + assertThat(intent.getStringExtra(CredentialsPickerActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(DefaultCombinedPicker.class.getName()); + } + + @Test + public void testInjectFragmentIntoIntent_workProfile() { + Intent intent = new Intent(); + + // Simulate managed / work profile. + when(mUserManager.isManagedProfile(anyInt())).thenReturn(true); + assertThat(DefaultCombinedPickerWork.isUserHandledByFragment(mUserManager, 10)).isTrue(); + + CredentialsPickerActivity.injectFragmentIntoIntent(mMockContext, intent); + assertThat(intent.getStringExtra(CredentialsPickerActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(DefaultCombinedPickerWork.class.getName()); + } + + @Test + public void testInjectFragmentIntoIntent_privateProfile() { + Intent intent = new Intent(); + + // Simulate private profile. + UserHandle privateUser = new UserHandle(100); + when(mUserManager.getUserInfo(100)) + .thenReturn(new UserInfo(100, "", "", 0, UserManager.USER_TYPE_PROFILE_PRIVATE)); + when(mUserManager.getUserProfiles()).thenReturn(Lists.newArrayList(privateUser)); + assertThat(DefaultCombinedPickerPrivate.isUserHandledByFragment(mUserManager)).isTrue(); + + CredentialsPickerActivity.injectFragmentIntoIntent(mMockContext, intent); + assertThat(intent.getStringExtra(CredentialsPickerActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(DefaultCombinedPickerPrivate.class.getName()); + } +} diff --git a/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java index 301fcfa6717..d02240e3786 100644 --- a/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java +++ b/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java @@ -87,16 +87,14 @@ public class DefaultCombinedPreferenceControllerTest { @Test public void ensureSettingsActivityIntentCreatedSuccessfully() { - DefaultCombinedPreferenceController dcpc = - new DefaultCombinedPreferenceController(mContext); - // Ensure that the settings activity is only created if we haved the right combination // of package and class name. - assertThat(dcpc.createSettingsActivityIntent(null, null)).isNull(); - assertThat(dcpc.createSettingsActivityIntent("", null)).isNull(); - assertThat(dcpc.createSettingsActivityIntent("", "")).isNull(); - assertThat(dcpc.createSettingsActivityIntent("com.test", "")).isNull(); - assertThat(dcpc.createSettingsActivityIntent("com.test", "ClassName")).isNotNull(); + assertThat(CombinedProviderInfo.createSettingsActivityIntent(null, null)).isNull(); + assertThat(CombinedProviderInfo.createSettingsActivityIntent("", null)).isNull(); + assertThat(CombinedProviderInfo.createSettingsActivityIntent("", "")).isNull(); + assertThat(CombinedProviderInfo.createSettingsActivityIntent("com.test", "")).isNull(); + assertThat(CombinedProviderInfo.createSettingsActivityIntent("com.test", "ClassName")) + .isNotNull(); } @Test @@ -112,13 +110,13 @@ public class DefaultCombinedPreferenceControllerTest { // Update the preference to use the provider and make sure the view // was updated. - dcpc.updatePreferenceForProvider(ppp, "App Name", "Subtitle", appIcon, null); + dcpc.updatePreferenceForProvider(ppp, "App Name", "Subtitle", appIcon, null, null); assertThat(ppp.getTitle().toString()).isEqualTo("App Name"); assertThat(ppp.getSummary().toString()).isEqualTo("Subtitle"); assertThat(ppp.getIcon()).isEqualTo(appIcon); // Set the preference back to none and make sure the view was updated. - dcpc.updatePreferenceForProvider(ppp, null, null, null, null); + dcpc.updatePreferenceForProvider(ppp, null, null, null, null, null); assertThat(ppp.getTitle().toString()).isEqualTo("None"); assertThat(ppp.getSummary()).isNull(); assertThat(ppp.getIcon()).isNull();