diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index aa3fe976a1d..476d4878253 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4204,6 +4204,12 @@
+
+
+
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3617f0986dc..11eaac18f01 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -10229,6 +10229,15 @@
Saved info like addresses or payment methods won\'t be filled in when you sign in. To keep your saved info filled in, set enable a password, passkey and data/or service.
+
+ Turn on %1$s\?
+
+
+ Saved info like addresses or payment methods will be shared with this provider.
+
+
+ Turn on
+
Passwords, passkeys and data services limit
diff --git a/src/com/android/settings/accounts/AccountDashboardFragment.java b/src/com/android/settings/accounts/AccountDashboardFragment.java
index bba28262bb5..f59de46428d 100644
--- a/src/com/android/settings/accounts/AccountDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountDashboardFragment.java
@@ -75,7 +75,9 @@ public class AccountDashboardFragment extends DashboardFragment {
if (CredentialManager.isServiceEnabled(context)) {
CredentialManagerPreferenceController cmpp =
use(CredentialManagerPreferenceController.class);
- cmpp.init(this, getFragmentManager());
+ CredentialManagerPreferenceController.Delegate delegate =
+ result -> getActivity().setResult(result);
+ cmpp.init(this, getFragmentManager(), getIntent(), delegate);
} else {
getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class));
}
diff --git a/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java b/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
index e0d49d2171a..a87eb7dd774 100644
--- a/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
@@ -69,7 +69,9 @@ public class AccountPersonalDashboardFragment extends DashboardFragment {
if (CredentialManager.isServiceEnabled(context)) {
CredentialManagerPreferenceController cmpp =
use(CredentialManagerPreferenceController.class);
- cmpp.init(this, getFragmentManager());
+ CredentialManagerPreferenceController.Delegate delegate =
+ result -> getActivity().setResult(result);
+ cmpp.init(this, getFragmentManager(), getIntent(), delegate);
} else {
getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class));
}
diff --git a/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java b/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
index da380b3b140..445aced18d0 100644
--- a/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
@@ -69,7 +69,9 @@ public class AccountWorkProfileDashboardFragment extends DashboardFragment {
if (CredentialManager.isServiceEnabled(context)) {
CredentialManagerPreferenceController cmpp =
use(CredentialManagerPreferenceController.class);
- cmpp.init(this, getFragmentManager());
+ CredentialManagerPreferenceController.Delegate delegate =
+ result -> getActivity().setResult(result);
+ cmpp.init(this, getFragmentManager(), getIntent(), delegate);
} else {
getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class));
}
diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
index 7ca3f909013..f4a3cd4dc4b 100644
--- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -20,10 +20,12 @@ import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Activity;
import android.app.Dialog;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
@@ -34,6 +36,7 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.OutcomeReceiver;
import android.os.UserHandle;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
import android.util.Log;
@@ -67,6 +70,8 @@ import java.util.concurrent.Executor;
public class CredentialManagerPreferenceController extends BasePreferenceController
implements LifecycleObserver {
private static final String TAG = "CredentialManagerPreferenceController";
+ private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
+ private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
private static final int MAX_SELECTABLE_PROVIDERS = 5;
private final PackageManager mPm;
@@ -76,8 +81,10 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
private final @Nullable CredentialManager mCredentialManager;
private final Executor mExecutor;
private final Map mPrefs = new HashMap<>(); // key is package name
+ private final List mPendingServiceInfos = new ArrayList<>();
private @Nullable FragmentManager mFragmentManager = null;
+ private @Nullable Delegate mDelegate = null;
public CredentialManagerPreferenceController(Context context, String preferenceKey) {
super(context, preferenceKey);
@@ -115,10 +122,110 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
*
* @param fragment the fragment to use as the parent
* @param fragmentManager the fragment manager to use
+ * @param intent the intent used to start the activity
+ * @param delegate the delegate to send results back to
*/
- public void init(DashboardFragment fragment, FragmentManager fragmentManager) {
+ public void init(
+ DashboardFragment fragment,
+ FragmentManager fragmentManager,
+ @Nullable Intent launchIntent,
+ @NonNull Delegate delegate) {
fragment.getSettingsLifecycle().addObserver(this);
mFragmentManager = fragmentManager;
+ setDelegate(delegate);
+ verifyReceivedIntent(launchIntent);
+ }
+
+ /**
+ * Parses and sets the package component name. Returns a boolean as to whether this was
+ * successful.
+ */
+ @VisibleForTesting
+ boolean verifyReceivedIntent(Intent launchIntent) {
+ if (launchIntent == null || launchIntent.getAction() == null) {
+ return false;
+ }
+
+ final String action = launchIntent.getAction();
+ final boolean isCredProviderAction =
+ TextUtils.equals(action, PRIMARY_INTENT);
+ final boolean isExistingAction = TextUtils.equals(action, ALTERNATE_INTENT);
+ final boolean isValid = isCredProviderAction || isExistingAction;
+
+ if (!isValid) {
+ return false;
+ }
+
+ // After this point we have received a set credential manager provider intent
+ // so we should return a cancelled result if the data we got is no good.
+ if (launchIntent.getData() == null) {
+ setActivityResult(Activity.RESULT_CANCELED);
+ return false;
+ }
+
+ String packageName = launchIntent.getData().getSchemeSpecificPart();
+ if (packageName == null) {
+ setActivityResult(Activity.RESULT_CANCELED);
+ return false;
+ }
+
+ mPendingServiceInfos.clear();
+ for (CredentialProviderInfo cpi : mServices) {
+ final ServiceInfo serviceInfo = cpi.getServiceInfo();
+ if (serviceInfo.packageName.equals(packageName)) {
+ mPendingServiceInfos.add(serviceInfo);
+ }
+ }
+
+ // Don't set the result as RESULT_OK here because we should wait for the user to
+ // enable the provider.
+ if (!mPendingServiceInfos.isEmpty()) {
+ return true;
+ }
+
+ setActivityResult(Activity.RESULT_CANCELED);
+ return false;
+ }
+
+ @VisibleForTesting
+ void setDelegate(Delegate delegate) {
+ mDelegate = delegate;
+ }
+
+ private void setActivityResult(int resultCode) {
+ if (mDelegate == null) {
+ Log.e(TAG, "Missing delegate");
+ return;
+ }
+ mDelegate.setActivityResult(resultCode);
+ }
+
+ private void handleIntent() {
+ List pendingServiceInfos = new ArrayList<>(mPendingServiceInfos);
+ mPendingServiceInfos.clear();
+ if (pendingServiceInfos.isEmpty()) {
+ return;
+ }
+
+ ServiceInfo serviceInfo = pendingServiceInfos.get(0);
+ ApplicationInfo appInfo = serviceInfo.applicationInfo;
+ CharSequence appName = "";
+ if (appInfo.nonLocalizedLabel != null) {
+ appName = appInfo.loadLabel(mPm);
+ }
+
+ // Stop if there is no name.
+ if (TextUtils.isEmpty(appName)) {
+ return;
+ }
+
+ NewProviderConfirmationDialogFragment fragment =
+ newNewProviderConfirmationDialogFragment(serviceInfo.packageName, appName);
+ if (fragment == null || mFragmentManager == null) {
+ return;
+ }
+
+ fragment.show(mFragmentManager, NewProviderConfirmationDialogFragment.TAG);
}
@OnLifecycleEvent(ON_CREATE)
@@ -139,6 +246,9 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
mServices.clear();
mServices.addAll(availableServices);
+ // If there is a pending dialog then show it.
+ handleIntent();
+
mEnabledPackageNames.clear();
for (CredentialProviderInfo cpi : availableServices) {
if (cpi.isEnabled()) {
@@ -362,6 +472,49 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
});
}
+ /** Create the new provider confirmation dialog. */
+ private @Nullable NewProviderConfirmationDialogFragment
+ newNewProviderConfirmationDialogFragment(
+ @NonNull String packageName, @NonNull CharSequence appName) {
+ DialogHost host =
+ new DialogHost() {
+ @Override
+ public void onDialogClick(int whichButton) {
+ completeEnableProviderDialogBox(whichButton, packageName);
+ }
+ };
+
+ return new NewProviderConfirmationDialogFragment(host, packageName, appName);
+ }
+
+ @VisibleForTesting
+ void completeEnableProviderDialogBox(int whichButton, String packageName) {
+ if (whichButton == DialogInterface.BUTTON_POSITIVE) {
+ if (togglePackageNameEnabled(packageName)) {
+ // Enable all prefs.
+ if (mPrefs.containsKey(packageName)) {
+ mPrefs.get(packageName).setChecked(true);
+ }
+ setActivityResult(Activity.RESULT_OK);
+ } else {
+ // There are too many providers so set the result as cancelled.
+ setActivityResult(Activity.RESULT_CANCELED);
+
+ // Show the error if too many enabled.
+ final DialogFragment fragment = newErrorDialogFragment();
+
+ if (fragment == null || mFragmentManager == null) {
+ return;
+ }
+
+ fragment.show(mFragmentManager, ErrorDialogFragment.TAG);
+ }
+ } else {
+ // The user clicked the cancel button so send that result back.
+ setActivityResult(Activity.RESULT_CANCELED);
+ }
+ }
+
private @Nullable ErrorDialogFragment newErrorDialogFragment() {
DialogHost host =
new DialogHost() {
@@ -401,10 +554,15 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
}
/** Called when the dialog button is clicked. */
- private interface DialogHost {
+ private static interface DialogHost {
void onDialogClick(int whichButton);
}
+ /** Called to send messages back to the parent fragment. */
+ public static interface Delegate {
+ void setActivityResult(int resultCode);
+ }
+
/** Dialog fragment parent class. */
private abstract static class CredentialManagerDialogFragment extends DialogFragment
implements DialogInterface.OnClickListener {
@@ -484,4 +642,45 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
getDialogHost().onDialogClick(which);
}
}
+
+ /**
+ * Confirmation dialog fragment shows a dialog to the user to confirm that they would like to
+ * enable the new provider.
+ */
+ public static class NewProviderConfirmationDialogFragment
+ extends CredentialManagerDialogFragment {
+
+ NewProviderConfirmationDialogFragment(
+ DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName) {
+ super(dialogHost);
+
+ final Bundle argument = new Bundle();
+ argument.putString(PACKAGE_NAME_KEY, packageName);
+ argument.putCharSequence(APP_NAME_KEY, appName);
+ setArguments(argument);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Bundle bundle = getArguments();
+ final Context context = getContext();
+ final String title =
+ context.getString(
+ R.string.credman_enable_confirmation_message_title,
+ bundle.getCharSequence(CredentialManagerDialogFragment.APP_NAME_KEY));
+
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(title)
+ .setMessage(context.getString(R.string.credman_enable_confirmation_message))
+ .setPositiveButton(
+ R.string.credman_enable_confirmation_message_positive_button, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ getDialogHost().onDialogClick(which);
+ }
+ }
}
diff --git a/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java
index 9cfa7ed20c9..fb014caf3fd 100644
--- a/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java
@@ -24,11 +24,16 @@ import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
+import android.app.Activity;
import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.ServiceInfo;
import android.credentials.CredentialProviderInfo;
+import android.net.Uri;
import android.os.Looper;
+import android.provider.Settings;
import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceCategory;
@@ -47,6 +52,7 @@ import org.junit.runner.RunWith;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
@RunWith(AndroidJUnit4.class)
@@ -55,6 +61,8 @@ public class CredentialManagerPreferenceControllerTest {
private Context mContext;
private PreferenceScreen mScreen;
private PreferenceCategory mCredentialsPreferenceCategory;
+ private CredentialManagerPreferenceController.Delegate mDelegate;
+ private Optional mReceivedResultCode;
private static final String TEST_PACKAGE_NAME_A = "com.android.providerA";
private static final String TEST_PACKAGE_NAME_B = "com.android.providerB";
@@ -62,6 +70,8 @@ public class CredentialManagerPreferenceControllerTest {
private static final String TEST_TITLE_APP_A = "test app A";
private static final String TEST_TITLE_APP_B = "test app B";
private static final String TEST_TITLE_SERVICE_C = "test service C1";
+ private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
+ private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
@Before
public void setUp() {
@@ -73,9 +83,17 @@ public class CredentialManagerPreferenceControllerTest {
mCredentialsPreferenceCategory = new PreferenceCategory(mContext);
mCredentialsPreferenceCategory.setKey("credentials_test");
mScreen.addPreference(mCredentialsPreferenceCategory);
+ mReceivedResultCode = Optional.empty();
+ mDelegate =
+ new CredentialManagerPreferenceController.Delegate() {
+ @Override
+ public void setActivityResult(int resultCode) {
+ mReceivedResultCode = Optional.of(resultCode);
+ }
+ };
}
- /*@Test
+ @Test
// Tests that getAvailabilityStatus() does not throw an exception if it's called before the
// Controller is initialized (this can happen during indexing).
public void getAvailabilityStatus_withoutInit_returnsUnavailable() {
@@ -252,7 +270,7 @@ public class CredentialManagerPreferenceControllerTest {
assertThat(enabledServices.size()).isEqualTo(1);
assertThat(enabledServices.contains("com.android.provider1/ClassA")).isFalse();
assertThat(enabledServices.contains("com.android.provider2/ClassA")).isTrue();
- }*/
+ }
@Test
public void displayPreference_withServices_preferencesAdded_sameAppShouldBeMerged() {
@@ -312,12 +330,120 @@ public class CredentialManagerPreferenceControllerTest {
assertThat(prefs.get(TEST_PACKAGE_NAME_C).isChecked()).isTrue();
}
+ @Test
+ public void handleIntentWithProviderServiceInfo_handleBadIntent_missingData() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+
+ // Create an intent with missing data.
+ Intent missingDataIntent = new Intent(PRIMARY_INTENT);
+ assertThat(controller.verifyReceivedIntent(missingDataIntent)).isFalse();
+ }
+
+ @Test
+ public void handleIntentWithProviderServiceInfo_handleBadIntent_successDialog() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+ String packageName = cpi.getServiceInfo().packageName;
+
+ // Create an intent with valid data.
+ Intent intent = new Intent(PRIMARY_INTENT);
+ intent.setData(Uri.parse("package:" + packageName));
+ assertThat(controller.verifyReceivedIntent(intent)).isTrue();
+ controller.completeEnableProviderDialogBox(DialogInterface.BUTTON_POSITIVE, packageName);
+ assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_OK);
+ }
+
+ @Test
+ public void handleIntentWithProviderServiceInfo_handleIntent_cancelDialog() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+ String packageName = cpi.getServiceInfo().packageName;
+
+ // Create an intent with valid data.
+ Intent intent = new Intent(PRIMARY_INTENT);
+ intent.setData(Uri.parse("package:" + packageName));
+ assertThat(controller.verifyReceivedIntent(intent)).isTrue();
+ controller.completeEnableProviderDialogBox(DialogInterface.BUTTON_NEGATIVE, packageName);
+ assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void handleOtherIntentWithProviderServiceInfo_handleBadIntent_missingData() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+
+ // Create an intent with missing data.
+ Intent missingDataIntent = new Intent(ALTERNATE_INTENT);
+ assertThat(controller.verifyReceivedIntent(missingDataIntent)).isFalse();
+ }
+
+ @Test
+ public void handleOtherIntentWithProviderServiceInfo_handleBadIntent_successDialog() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+ String packageName = cpi.getServiceInfo().packageName;
+
+ // Create an intent with valid data.
+ Intent intent = new Intent(ALTERNATE_INTENT);
+ intent.setData(Uri.parse("package:" + packageName));
+ assertThat(controller.verifyReceivedIntent(intent)).isTrue();
+ controller.completeEnableProviderDialogBox(DialogInterface.BUTTON_POSITIVE, packageName);
+ assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_OK);
+ }
+
+ @Test
+ public void handleOtherIntentWithProviderServiceInfo_handleIntent_cancelDialog() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+ String packageName = cpi.getServiceInfo().packageName;
+
+ // Create an intent with valid data.
+ Intent intent = new Intent(ALTERNATE_INTENT);
+ intent.setData(Uri.parse("package:" + packageName));
+ assertThat(controller.verifyReceivedIntent(intent)).isTrue();
+ controller.completeEnableProviderDialogBox(DialogInterface.BUTTON_NEGATIVE, packageName);
+ assertThat(mReceivedResultCode.get()).isEqualTo(Activity.RESULT_CANCELED);
+ }
+
+ @Test
+ public void handleIntentWithProviderServiceInfo_handleIntent_incorrectAction() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+ String packageName = cpi.getServiceInfo().packageName;
+
+ // Create an intent with valid data.
+ Intent intent = new Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE);
+ intent.setData(Uri.parse("package:" + packageName));
+ assertThat(controller.verifyReceivedIntent(intent)).isFalse();
+ assertThat(mReceivedResultCode.isPresent()).isFalse();
+ }
+
+ @Test
+ public void handleIntentWithProviderServiceInfo_handleNullIntent() {
+ CredentialProviderInfo cpi = createCredentialProviderInfo();
+ CredentialManagerPreferenceController controller =
+ createControllerWithServices(Lists.newArrayList(cpi));
+
+ // Use a null intent.
+ assertThat(controller.verifyReceivedIntent(null)).isFalse();
+ assertThat(mReceivedResultCode.isPresent()).isFalse();
+ }
+
private CredentialManagerPreferenceController createControllerWithServices(
List availableServices) {
CredentialManagerPreferenceController controller =
new CredentialManagerPreferenceController(
mContext, mCredentialsPreferenceCategory.getKey());
controller.setAvailableServices(() -> mock(Lifecycle.class), availableServices);
+ controller.setDelegate(mDelegate);
return controller;
}