From 9d74509888b7dd65b287bc68b9445d9e23809cce Mon Sep 17 00:00:00 2001 From: Becca Hughes Date: Fri, 3 Feb 2023 00:04:21 +0000 Subject: [PATCH] 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 Change-Id: Id88cc7b3bf2829d075fbba87ea5dc0a245b9ae32 --- AndroidManifest.xml | 6 + res/values/strings.xml | 9 + .../accounts/AccountDashboardFragment.java | 4 +- .../AccountPersonalDashboardFragment.java | 4 +- .../AccountWorkProfileDashboardFragment.java | 4 +- ...CredentialManagerPreferenceController.java | 202 +++++++++++++++++- ...entialManagerPreferenceControllerTest.java | 83 +++++++ 7 files changed, 307 insertions(+), 5 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index dc519e95928..199fe5a5c7e 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -4200,6 +4200,12 @@ + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 90d367dd51b..f868529b7dd 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -10136,6 +10136,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 a1c4f67a480..3ec77829b76 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,7 @@ 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 int MAX_SELECTABLE_PROVIDERS = 5; private final PackageManager mPm; @@ -76,8 +80,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 +121,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, Settings.ACTION_CREDENTIAL_PROVIDER); + 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 +245,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()) { @@ -360,6 +469,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() { @@ -399,10 +551,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 { @@ -482,4 +639,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 47de08300de..cba3101986c 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; @@ -48,6 +53,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; @RunWith(AndroidJUnit4.class) @@ -56,6 +62,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"; @@ -74,6 +82,14 @@ 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 @@ -316,12 +332,79 @@ public class CredentialManagerPreferenceControllerTest { assertThat(pref3.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(Settings.ACTION_CREDENTIAL_PROVIDER); + 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(Settings.ACTION_CREDENTIAL_PROVIDER); + 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(Settings.ACTION_CREDENTIAL_PROVIDER); + 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; }