Add settings intent dialog

Add a dialog that can be launched via
an intent to prompt the user to enable
the provider for credman.

Test: make & atest & manual
Bug: 267816998
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:9d74509888b7dd65b287bc68b9445d9e23809cce)
Merged-In: Id88cc7b3bf2829d075fbba87ea5dc0a245b9ae32

Change-Id: Id88cc7b3bf2829d075fbba87ea5dc0a245b9ae32
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index dc519e9..199fe5a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -4200,6 +4200,12 @@
                 <action android:name="android.settings.SYNC_SETTINGS" />
                 <category android:name="android.intent.category.BROWSABLE" />
                 <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="package" />
+            </intent-filter>
+            <intent-filter android:priority="1">
+                <action android:name="android.settings.CREDENTIAL_PROVIDER" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="package" />
             </intent-filter>
             <intent-filter android:priority="53">
                 <action android:name="android.intent.action.MAIN" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2538245..0216d28 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -10229,6 +10229,15 @@
     <!-- Message of the warning dialog for disabling the credential provider. [CHAR_LIMIT=NONE] -->
     <string name="credman_confirmation_message">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.</string>
 
+    <!-- Title of the warning dialog for enabling the credential provider. [CHAR_LIMIT=NONE] -->
+    <string name="credman_enable_confirmation_message_title">Turn on %1$s\?</string>
+
+    <!-- Message of the warning dialog for enabling the credential provider. [CHAR_LIMIT=NONE] -->
+    <string name="credman_enable_confirmation_message">Saved info like addresses or payment methods will be shared with this provider.</string>
+
+    <!-- Positive button to turn on credential manager provider (confirmation). [CHAR LIMIT=60] -->
+    <string name="credman_enable_confirmation_message_positive_button">Turn on</string>
+
     <!-- Title of the error dialog when too many credential providers are selected. [CHAR_LIMIT=NONE] -->
     <string name="credman_error_message_title">Passwords, passkeys and data services limit</string>
 
diff --git a/src/com/android/settings/accounts/AccountDashboardFragment.java b/src/com/android/settings/accounts/AccountDashboardFragment.java
index bba2826..f59de46 100644
--- a/src/com/android/settings/accounts/AccountDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountDashboardFragment.java
@@ -75,7 +75,9 @@
         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 e0d49d2..a87eb7d 100644
--- a/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
@@ -69,7 +69,9 @@
         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 da380b3..445aced 100644
--- a/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
@@ -69,7 +69,9 @@
         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 e16d0d8..ce22c32 100644
--- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -20,10 +20,12 @@
 
 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.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 @@
 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 @@
     private final @Nullable CredentialManager mCredentialManager;
     private final Executor mExecutor;
     private final Map<String, SwitchPreference> mPrefs = new HashMap<>(); // key is package name
+    private final List<ServiceInfo> 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 @@
      *
      * @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<ServiceInfo> 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 @@
         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 @@
                 });
     }
 
+    /** 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 @@
     }
 
     /** 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 @@
             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 9cfa7ed..fb014ca 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 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 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 @@
     private Context mContext;
     private PreferenceScreen mScreen;
     private PreferenceCategory mCredentialsPreferenceCategory;
+    private CredentialManagerPreferenceController.Delegate mDelegate;
+    private Optional<Integer> 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 @@
     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 @@
         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 @@
         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 @@
         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<CredentialProviderInfo> availableServices) {
         CredentialManagerPreferenceController controller =
                 new CredentialManagerPreferenceController(
                         mContext, mCredentialsPreferenceCategory.getKey());
         controller.setAvailableServices(() -> mock(Lifecycle.class), availableServices);
+        controller.setDelegate(mDelegate);
         return controller;
     }