From fa433271a8633af93e1b72d15fe3cf50e1c98142 Mon Sep 17 00:00:00 2001 From: Dongzhuo Zhang Date: Mon, 7 Oct 2024 22:02:13 +0000 Subject: [PATCH] Add Contacts Stroage fragment class for Contacts Storage settings page. Test: atest SettingsRoboTests:com.android.settings.applications.contacts.ContactsStorageSettingsTest Bug: 368641291 Flag: com.android.settings.flags.enable_contacts_default_account_in_settings Change-Id: I6ece3eeea64d07d4a97574f67549a72267371df8 --- res/drawable/cloud_off.xml | 24 ++ res/values/strings.xml | 8 + res/xml/apps.xml | 1 + res/xml/contacts_storage_settings.xml | 32 +++ .../contacts/ContactsStorageSettings.java | 195 ++++++++++++++ .../contacts/ContactsStorageSettingsTest.java | 240 ++++++++++++++++++ 6 files changed, 500 insertions(+) create mode 100644 res/drawable/cloud_off.xml create mode 100644 res/xml/contacts_storage_settings.xml create mode 100644 src/com/android/settings/applications/contacts/ContactsStorageSettings.java create mode 100644 tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java diff --git a/res/drawable/cloud_off.xml b/res/drawable/cloud_off.xml new file mode 100644 index 00000000000..cef728faf00 --- /dev/null +++ b/res/drawable/cloud_off.xml @@ -0,0 +1,24 @@ + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 600c08ce70d..7ff00ce1fab 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -13691,6 +13691,14 @@ contacts, storage, account Contacts storage + + Device only + + New contacts won\'t be synced with an account + + Contacts will be saved to your device and synced to your account by default No default set + + Add an account to get started diff --git a/res/xml/apps.xml b/res/xml/apps.xml index a66cea3d698..77b210f1821 100644 --- a/res/xml/apps.xml +++ b/res/xml/apps.xml @@ -85,6 +85,7 @@ android:summary="@string/summary_placeholder" android:order="13" settings:controller="com.android.settings.applications.contacts.ContactsStoragePreferenceController" + android:fragment="com.android.settings.applications.contacts.ContactsStorageSettings" settings:keywords="@string/keywords_contacts_storage"> diff --git a/res/xml/contacts_storage_settings.xml b/res/xml/contacts_storage_settings.xml new file mode 100644 index 00000000000..7cbabe725ce --- /dev/null +++ b/res/xml/contacts_storage_settings.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/src/com/android/settings/applications/contacts/ContactsStorageSettings.java b/src/com/android/settings/applications/contacts/ContactsStorageSettings.java new file mode 100644 index 00000000000..90d593a0c7e --- /dev/null +++ b/src/com/android/settings/applications/contacts/ContactsStorageSettings.java @@ -0,0 +1,195 @@ + +/* + * Copyright (C) 2020 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.contacts; + +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.provider.Settings.ACTION_ADD_ACCOUNT; +import static android.provider.Settings.EXTRA_ACCOUNT_TYPES; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.UserHandle; +import android.provider.ContactsContract.Settings; + +import androidx.annotation.NonNull; +import androidx.annotation.UiThread; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceClickListener; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.accounts.AddAccountSettings; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.accounts.AuthenticatorHelper; +import com.android.settingslib.search.SearchIndexable; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.HashMap; +import java.util.Map; + +/** + * Settings page for contacts default account + */ +@SearchIndexable +public class ContactsStorageSettings extends DashboardFragment + implements SelectorWithWidgetPreference.OnClickListener, OnPreferenceClickListener { + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider(R.xml.contacts_storage_settings); + private static final String TAG = "ContactsStorageSettings"; + private static final String PREF_KEY_ADD_ACCOUNT = "add_account"; + private static final String PREF_KEY_DEVICE_ONLY = "device_only_account_preference"; + private final Map mAccountMap = new HashMap<>(); + private AuthenticatorHelper mAuthenticatorHelper; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mAuthenticatorHelper = new AuthenticatorHelper(context, + new UserHandle(UserHandle.myUserId()), null); + } + + @UiThread + @Override + public void onRadioButtonClicked(@NonNull SelectorWithWidgetPreference selectedPref) { + final String selectedPreferenceKey = selectedPref.getKey(); + // Check if current provider is different from the selected provider. + for (String preferenceKey : mAccountMap.keySet()) { + if (selectedPreferenceKey.equals(preferenceKey)) { + selectedPref.setChecked(true); + //TODO: Call DefaultAccount.setDefaultAccountForNewContacts once + // the implementation is ready. + Settings.setDefaultAccount(getContentResolver(), mAccountMap.get(preferenceKey)); + } else { + SelectorWithWidgetPreference unSelectedPreference = + getPreferenceScreen().findPreference(preferenceKey); + if (unSelectedPreference != null) { + unSelectedPreference.setChecked(false); + } + } + } + } + + public boolean onPreferenceClick(@NonNull Preference preference) { + if (PREF_KEY_ADD_ACCOUNT.equals(preference.getKey())) { + Resources resources = Resources.getSystem(); + String[] accountTypesArray = + resources.getStringArray( + com.android.internal.R.array.config_rawContactsEligibleDefaultAccountTypes); + Intent intent = new Intent(ACTION_ADD_ACCOUNT); + intent.setClass(getContext(), AddAccountSettings.class); + intent.putExtra(EXTRA_ACCOUNT_TYPES, accountTypesArray); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); + return true; + } + return false; + } + + @Override + public void onCreatePreferences(@NonNull Bundle savedInstanceState, + @NonNull String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + refreshUI(); + } + + @UiThread + void refreshUI() { + // Clear all the accounts stored in the map and later on re-fetch the eligible accounts + // when creating eligible account preferences. + mAccountMap.clear(); + final PreferenceScreen screen = getPreferenceScreen(); + AccountManager accountManager = AccountManager.get(getPrefContext()); + //TODO: Call DefaultAccount.getDefaultAccountForNewContacts once + // implementation is ready. + Account[] accounts = accountManager.getAccounts(); + + for (int i = 0; i < accounts.length; i++) { + screen.addPreference(buildAccountPreference(accounts[i], i)); + } + screen.addPreference(buildAddAccountPreference(accounts.length == 0)); + setupDeviceOnlyPreference(); + + //TODO: Call DefaultAccount.ListEligibleCloudAccounts once the + // implementation is ready. And differentiate device only account vs account not set case. + Account currentDefaultAccount = Settings.getDefaultAccount(getContentResolver()); + String preferenceKey = currentDefaultAccount != null ? + String.valueOf(currentDefaultAccount.hashCode()) : PREF_KEY_DEVICE_ONLY; + SelectorWithWidgetPreference preference = getPreferenceScreen().findPreference( + preferenceKey); + if (preference != null) { + preference.setChecked(true); + } + } + + private void setupDeviceOnlyPreference() { + SelectorWithWidgetPreference preference = findPreference(PREF_KEY_DEVICE_ONLY); + if (preference != null) { + preference.setOnClickListener(this); + mAccountMap.put(PREF_KEY_DEVICE_ONLY, null); + } + } + + //TODO: Add preference category on account preferences. + private Preference buildAccountPreference(Account account, int order) { + SelectorWithWidgetPreference preference = new SelectorWithWidgetPreference( + getPrefContext()); + preference.setTitle(mAuthenticatorHelper.getLabelForType(getPrefContext(), account.type)); + preference.setIcon(mAuthenticatorHelper.getDrawableForType(getPrefContext(), account.type)); + preference.setSummary(account.name); + preference.setKey(String.valueOf(account.hashCode())); + preference.setOnClickListener(this); + preference.setOrder(order); + mAccountMap.put(String.valueOf(account.hashCode()), account); + return preference; + } + + private RestrictedPreference buildAddAccountPreference(boolean noAccountBeenAdded) { + RestrictedPreference preference = new RestrictedPreference(getPrefContext()); + preference.setKey(PREF_KEY_ADD_ACCOUNT); + if (noAccountBeenAdded) { + preference.setTitle(R.string.contacts_storage_first_time_add_account_message); + } else { + preference.setTitle(R.string.add_account_label); + } + preference.setIcon(R.drawable.ic_add_24dp); + preference.setOnPreferenceClickListener(this); + preference.setOrder(998); + return preference; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.contacts_storage_settings; + } + + @Override + protected String getLogTag() { + return TAG; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.CONTACTS_STORAGE; + } +} diff --git a/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java b/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java new file mode 100644 index 00000000000..785ef4a9d11 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/contacts/ContactsStorageSettingsTest.java @@ -0,0 +1,240 @@ +/* + * 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.contacts; + +import static android.provider.ContactsContract.Settings.KEY_DEFAULT_ACCOUNT; +import static android.provider.ContactsContract.Settings.QUERY_DEFAULT_ACCOUNT_METHOD; +import static android.provider.ContactsContract.Settings.SET_DEFAULT_ACCOUNT_METHOD; +import static android.provider.Settings.ACTION_ADD_ACCOUNT; +import static android.provider.Settings.EXTRA_ACCOUNT_TYPES; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.app.settings.SettingsEnums; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.os.Bundle; +import android.provider.ContactsContract; +import android.provider.SearchIndexableResource; + +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; + +import com.android.settings.R; +import com.android.settings.accounts.AddAccountSettings; +import com.android.settings.testutils.shadow.ShadowAuthenticationHelper; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowAuthenticationHelper.class) +public class ContactsStorageSettingsTest { + private static final String PREF_KEY_DEVICE_ONLY = "device_only_account_preference"; + + private static final String PREF_KEY_ADD_ACCOUNT = "add_account"; + + private static final Account TEST_ACCOUNT1 = new Account("test@gmail.com", "type1"); + + private static final Account TEST_ACCOUNT2 = new Account("test@samsung.com", "type2"); + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + @Spy + public final Context mContext = spy(ApplicationProvider.getApplicationContext()); + @Mock + private ContentResolver mContentResolver; + @Mock + private AccountManager mAccountManager; + + private PreferenceManager mPreferenceManager; + private TestContactsStorageSettings mContactsStorageSettings; + private PreferenceScreen mScreen; + + @Before + public void setUp() throws Exception { + mContactsStorageSettings = spy(new TestContactsStorageSettings(mContext, mContentResolver)); + when(mContext.getSystemService(eq(Context.ACCOUNT_SERVICE))).thenReturn(mAccountManager); + when(mAccountManager.getAccountsAsUser(anyInt())).thenReturn(new Account[]{}); + mPreferenceManager = new PreferenceManager(mContext); + when(mContactsStorageSettings.getPreferenceManager()).thenReturn(mPreferenceManager); + mScreen = spy(new PreferenceScreen(mContext, /* attrs= */ null)); + when(mScreen.getPreferenceManager()).thenReturn(mPreferenceManager); + when(mContactsStorageSettings.getPreferenceScreen()).thenReturn(mScreen); + mContactsStorageSettings.onAttach(mContext); + } + + @Test + public void getMetricsCategory() { + assertThat(mContactsStorageSettings.getMetricsCategory()).isEqualTo( + SettingsEnums.CONTACTS_STORAGE); + } + + @Test + public void getPreferenceScreenResId() { + assertThat(mContactsStorageSettings.getPreferenceScreenResId()).isEqualTo( + R.xml.contacts_storage_settings); + } + + @Test + public void verifyDeviceOnlyPreference_onClick_setDefaultAccountToNull() { + when(mAccountManager.getAccounts()).thenReturn(new Account[]{}); + Bundle bundle = new Bundle(); + bundle.putParcelable(KEY_DEFAULT_ACCOUNT, null); + when(mContentResolver.call(eq(ContactsContract.AUTHORITY_URI), + eq(QUERY_DEFAULT_ACCOUNT_METHOD), any(), any())).thenReturn(bundle); + + PreferenceScreen settingScreen = mPreferenceManager.inflateFromResource(mContext, + R.xml.contacts_storage_settings, mScreen); + SelectorWithWidgetPreference deviceOnlyPreference = settingScreen.findPreference( + PREF_KEY_DEVICE_ONLY); + when(mContactsStorageSettings.findPreference(eq(PREF_KEY_DEVICE_ONLY))).thenReturn( + deviceOnlyPreference); + + assertThat(deviceOnlyPreference.getTitle()).isEqualTo("Device only"); + assertThat(deviceOnlyPreference.getSummary()).isEqualTo( + "New contacts won't be synced with an account"); + assertThat(deviceOnlyPreference.getOrder()).isEqualTo(999); + + mContactsStorageSettings.refreshUI(); + mContactsStorageSettings.onRadioButtonClicked(deviceOnlyPreference); + + assertThat(deviceOnlyPreference.isChecked()).isTrue(); + ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); + verify(mContentResolver).call(eq(ContactsContract.AUTHORITY_URI), + eq(SET_DEFAULT_ACCOUNT_METHOD), any(), captor.capture()); + Bundle accountBundle = captor.getValue(); + assertThat(accountBundle.getString(ContactsContract.Settings.ACCOUNT_NAME)).isNull(); + assertThat(accountBundle.getString(ContactsContract.Settings.ACCOUNT_TYPE)).isNull(); + } + + @Test + public void verifyAddAccountPreference_onClick_startAddAccountActivity() { + when(mAccountManager.getAccounts()).thenReturn(new Account[]{}); + when(mContentResolver.call(eq(ContactsContract.AUTHORITY_URI), + eq(QUERY_DEFAULT_ACCOUNT_METHOD), any(), any())).thenReturn(Bundle.EMPTY); + + mContactsStorageSettings.refreshUI(); + + assertThat(mScreen.findPreference(PREF_KEY_ADD_ACCOUNT).getTitle()).isEqualTo( + "Add an account to get started"); + assertThat(mScreen.findPreference(PREF_KEY_ADD_ACCOUNT).getOrder()).isEqualTo(998); + + mScreen.findPreference(PREF_KEY_ADD_ACCOUNT).performClick(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Intent.class); + verify(mContext).startActivity(captor.capture()); + Intent addAccountIntent = captor.getValue(); + assertThat(addAccountIntent.getAction()).isEqualTo(ACTION_ADD_ACCOUNT); + assertThat(addAccountIntent.getComponent().getClassName()).isEqualTo( + AddAccountSettings.class.getCanonicalName()); + String[] eligibleAccounts = (String[]) addAccountIntent.getExtra(EXTRA_ACCOUNT_TYPES); + assertThat(eligibleAccounts).isEmpty(); + } + + @Test + public void verifyEligibleAccountPreference_onClick_setSelectedDefaultAccount() { + when(mAccountManager.getAccounts()).thenReturn(new Account[]{TEST_ACCOUNT1, TEST_ACCOUNT2}); + Bundle bundle = new Bundle(); + bundle.putParcelable(KEY_DEFAULT_ACCOUNT, TEST_ACCOUNT2); + when(mContentResolver.call(eq(ContactsContract.AUTHORITY_URI), + eq(QUERY_DEFAULT_ACCOUNT_METHOD), any(), any())).thenReturn(bundle); + + mContactsStorageSettings.refreshUI(); + + SelectorWithWidgetPreference account1Preference = mScreen.findPreference( + String.valueOf(TEST_ACCOUNT1.hashCode())); + assertThat(account1Preference.getTitle()).isEqualTo("LABEL1"); + assertThat(account1Preference.getSummary()).isEqualTo("test@gmail.com"); + assertThat(account1Preference.getIcon()).isNotNull(); + + SelectorWithWidgetPreference account2Preference = mScreen.findPreference( + String.valueOf(TEST_ACCOUNT2.hashCode())); + assertThat(account2Preference.getTitle()).isEqualTo("LABEL2"); + assertThat(account2Preference.getSummary()).isEqualTo("test@samsung.com"); + assertThat(account2Preference.getIcon()).isNotNull(); + + mContactsStorageSettings.onRadioButtonClicked(account2Preference); + assertThat(account1Preference.isChecked()).isFalse(); + assertThat(account2Preference.isChecked()).isTrue(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Bundle.class); + verify(mContentResolver).call(eq(ContactsContract.AUTHORITY_URI), + eq(SET_DEFAULT_ACCOUNT_METHOD), any(), captor.capture()); + Bundle setAccountBundle = captor.getValue(); + assertThat(setAccountBundle.getString(ContactsContract.Settings.ACCOUNT_NAME)).isEqualTo( + "test@samsung.com"); + assertThat(setAccountBundle.getString(ContactsContract.Settings.ACCOUNT_TYPE)).isEqualTo( + "type2"); + } + + @Test + public void searchIndexProvider_shouldIndexResource() { + final List indexRes = + ContactsStorageSettings.SEARCH_INDEX_DATA_PROVIDER.getXmlResourcesToIndex( + RuntimeEnvironment.application, true /* enabled */); + + assertThat(indexRes).isNotNull(); + assertThat(indexRes.get(0).xmlResId).isEqualTo( + mContactsStorageSettings.getPreferenceScreenResId()); + } + + private static class TestContactsStorageSettings extends ContactsStorageSettings { + private final Context mContext; + private final ContentResolver mContentResolver; + + TestContactsStorageSettings(Context context, ContentResolver contentResolver) { + mContext = context; + mContentResolver = contentResolver; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + protected ContentResolver getContentResolver() { + // Override it so we can access this method in test + return mContentResolver; + } + } +}