diff --git a/res/xml/account_type_settings.xml b/res/xml/account_type_settings.xml index 0ba961f2153..7f57ed0ab4e 100644 --- a/res/xml/account_type_settings.xml +++ b/res/xml/account_type_settings.xml @@ -39,6 +39,7 @@ android:key="remove_account" android:layout="@layout/remove_account_button" android:order="1000" - android:selectable="false"/> + android:selectable="false" + settings:allowDividerAbove="true"/> diff --git a/src/com/android/settings/accounts/AccountSyncPreferenceController.java b/src/com/android/settings/accounts/AccountSyncPreferenceController.java index 847bb038ee1..3eed037d213 100644 --- a/src/com/android/settings/accounts/AccountSyncPreferenceController.java +++ b/src/com/android/settings/accounts/AccountSyncPreferenceController.java @@ -19,25 +19,32 @@ package com.android.settings.accounts; import static android.content.Intent.EXTRA_USER; import android.accounts.Account; +import android.content.ContentResolver; import android.content.Context; +import android.content.SyncAdapterType; import android.os.Bundle; import android.os.UserHandle; +import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.accounts.AuthenticatorHelper; import com.android.settingslib.core.AbstractPreferenceController; public class AccountSyncPreferenceController extends AbstractPreferenceController - implements PreferenceControllerMixin { + implements PreferenceControllerMixin, AuthenticatorHelper.OnAccountsUpdateListener { private static final String TAG = "AccountSyncController"; private static final String KEY_ACCOUNT_SYNC = "account_sync"; private Account mAccount; private UserHandle mUserHandle; + private AuthenticatorHelper mAuthenticatorHelper; + private Preference mPreference; public AccountSyncPreferenceController(Context context) { super(context); @@ -67,8 +74,64 @@ public class AccountSyncPreferenceController extends AbstractPreferenceControlle return KEY_ACCOUNT_SYNC; } + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public void updateState(Preference preference) { + updateSummary(preference); + } + + @Override + public void onAccountsUpdate(UserHandle userHandle) { + updateSummary(mPreference); + } + public void init(Account account, UserHandle userHandle) { mAccount = account; mUserHandle = userHandle; + mAuthenticatorHelper = new AuthenticatorHelper(mContext, mUserHandle, this); + } + + @VisibleForTesting + void updateSummary(Preference preference) { + if (mAccount == null) { + return; + } + final int userId = mUserHandle.getIdentifier(); + final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(userId); + int total = 0; + int enabled = 0; + if (syncAdapters != null) { + for (int i = 0, n = syncAdapters.length; i < n; i++) { + final SyncAdapterType sa = syncAdapters[i]; + if (!sa.accountType.equals(mAccount.type) || !sa.isUserVisible()) { + continue; + } + final int syncState = + ContentResolver.getIsSyncableAsUser(mAccount, sa.authority, userId); + if (syncState > 0) { + total++; + final boolean syncEnabled = ContentResolver.getSyncAutomaticallyAsUser( + mAccount, sa.authority, userId); + final boolean oneTimeSyncMode = + !ContentResolver.getMasterSyncAutomaticallyAsUser(userId); + if (oneTimeSyncMode || syncEnabled) { + enabled++; + } + } + } + } + if (enabled == 0) { + preference.setSummary(R.string.account_sync_summary_all_off); + } else if (enabled == total) { + preference.setSummary(R.string.account_sync_summary_all_on); + } else { + preference.setSummary( + mContext.getString(R.string.account_sync_summary_some_on, enabled, total)); + } } } diff --git a/src/com/android/settings/applications/LayoutPreference.java b/src/com/android/settings/applications/LayoutPreference.java index 6ae07720284..f2bd183bfb0 100644 --- a/src/com/android/settings/applications/LayoutPreference.java +++ b/src/com/android/settings/applications/LayoutPreference.java @@ -19,6 +19,7 @@ package com.android.settings.applications; import android.content.Context; import android.content.res.TypedArray; import android.support.annotation.VisibleForTesting; +import android.support.v4.content.res.TypedArrayUtils; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceViewHolder; import android.util.AttributeSet; @@ -33,19 +34,30 @@ import com.android.settings.Utils; public class LayoutPreference extends Preference { private final View.OnClickListener mClickListener = v -> performClick(v); + private boolean mAllowDividerAbove; + private boolean mAllowDividerBelow; @VisibleForTesting View mRootView; public LayoutPreference(Context context, AttributeSet attrs) { super(context, attrs); - final TypedArray a = context.obtainStyledAttributes( + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Preference); + mAllowDividerAbove = TypedArrayUtils.getBoolean(a, R.styleable.Preference_allowDividerAbove, + R.styleable.Preference_allowDividerAbove, false); + mAllowDividerBelow = TypedArrayUtils.getBoolean(a, R.styleable.Preference_allowDividerBelow, + R.styleable.Preference_allowDividerBelow, false); + a.recycle(); + + a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.Preference, 0, 0); int layoutResource = a.getResourceId(com.android.internal.R.styleable.Preference_layout, 0); if (layoutResource == 0) { throw new IllegalArgumentException("LayoutPreference requires a layout to be defined"); } + a.recycle(); + // Need to create view now so that findViewById can be called immediately. final View view = LayoutInflater.from(getContext()) .inflate(layoutResource, null, false); @@ -78,6 +90,8 @@ public class LayoutPreference extends Preference { final boolean selectable = isSelectable(); holder.itemView.setFocusable(selectable); holder.itemView.setClickable(selectable); + holder.setDividerAllowedAbove(mAllowDividerAbove); + holder.setDividerAllowedBelow(mAllowDividerBelow); FrameLayout layout = (FrameLayout) holder.itemView; layout.removeAllViews(); diff --git a/tests/robotests/src/com/android/settings/accounts/AccountSyncPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/accounts/AccountSyncPreferenceControllerTest.java index edb5d89bcd8..5fbd3a706a4 100644 --- a/tests/robotests/src/com/android/settings/accounts/AccountSyncPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/accounts/AccountSyncPreferenceControllerTest.java @@ -16,11 +16,17 @@ package com.android.settings.accounts; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; + +import static org.mockito.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.when; import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorDescription; import android.content.Context; import android.content.Intent; +import android.content.SyncAdapterType; import android.os.UserHandle; import android.support.v7.preference.Preference; @@ -28,29 +34,58 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowAccountManager; +import com.android.settings.testutils.shadow.ShadowContentResolver; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowApplication; @RunWith(SettingsRobolectricTestRunner.class) -@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, + shadows = {ShadowAccountManager.class, ShadowContentResolver.class}) public class AccountSyncPreferenceControllerTest { + @Mock(answer = RETURNS_DEEP_STUBS) + private AccountManager mAccountManager; + + private Context mContext; + private AccountSyncPreferenceController mController; + private Preference mPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowApplication application = ShadowApplication.getInstance(); + application.setSystemService(Context.ACCOUNT_SERVICE, mAccountManager); + mContext = application.getApplicationContext(); + + when(mAccountManager.getAuthenticatorTypesAsUser(anyInt())).thenReturn( + new AuthenticatorDescription[0]); + when(mAccountManager.getAccountsAsUser(anyInt())).thenReturn(new Account[0]); + + mPreference = new Preference(mContext); + mPreference.setKey("account_sync"); + + mController = new AccountSyncPreferenceController(mContext); + mController.init(new Account("acct1", "type1"), new UserHandle(3)); + } + + @After + public void tearDown() { + ShadowContentResolver.reset(); + } + @Test public void handlePreferenceTreeClick_shouldStartFragment() { - final ShadowApplication application = ShadowApplication.getInstance(); - final Context context = application.getApplicationContext(); - final Preference preference = new Preference(context); - preference.setKey("account_sync"); + mController.handlePreferenceTreeClick(mPreference); - final AccountSyncPreferenceController controller = - new AccountSyncPreferenceController(context); - controller.init(new Account("acct1", "type1"), mock(UserHandle.class)); - controller.handlePreferenceTreeClick(preference); - - final Intent nextActivity = application.getNextStartedActivity(); + final Intent nextActivity = ShadowApplication.getInstance().getNextStartedActivity(); assertThat(nextActivity.getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) .isEqualTo(AccountSyncSettings.class.getName()); @@ -58,4 +93,93 @@ public class AccountSyncPreferenceControllerTest { .isEqualTo(R.string.account_sync_title); } + @Test + public void updateSummary_adapterInvisible_shouldNotCount() { + SyncAdapterType syncAdapterType = new SyncAdapterType("authority" /* authority */, + "type1" /* accountType */, false /* userVisible */, true /* supportsUploading */); + SyncAdapterType[] syncAdapters = {syncAdapterType}; + ShadowContentResolver.setSyncAdapterTypes(syncAdapters); + + mController.updateSummary(mPreference); + + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.account_sync_summary_all_off)); + } + + @Test + public void updateSummary_notSameAccountType_shouldNotCount() { + SyncAdapterType syncAdapterType = new SyncAdapterType("authority" /* authority */, + "type5" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType[] syncAdapters = {syncAdapterType}; + ShadowContentResolver.setSyncAdapterTypes(syncAdapters); + + mController.updateSummary(mPreference); + + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.account_sync_summary_all_off)); + } + + @Test + public void updateSummary_notSyncable_shouldNotCount() { + SyncAdapterType syncAdapterType = new SyncAdapterType("authority" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType[] syncAdapters = {syncAdapterType}; + ShadowContentResolver.setSyncAdapterTypes(syncAdapters); + ShadowContentResolver.setSyncable("authority", 0); + + mController.updateSummary(mPreference); + + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.account_sync_summary_all_off)); + } + + @Test + public void updateSummary_syncDisabled_shouldNotCount() { + SyncAdapterType syncAdapterType = new SyncAdapterType("authority" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType[] syncAdapters = {syncAdapterType}; + ShadowContentResolver.setSyncAdapterTypes(syncAdapters); + ShadowContentResolver.setSyncAutomatically("authority", false); + ShadowContentResolver.setMasterSyncAutomatically(3, true); + + mController.updateSummary(mPreference); + + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.account_sync_summary_all_off)); + } + + @Test + public void updateSummary_syncEnabled_shouldCount() { + SyncAdapterType syncAdapterType = new SyncAdapterType("authority" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType[] syncAdapters = {syncAdapterType}; + ShadowContentResolver.setSyncAdapterTypes(syncAdapters); + + mController.updateSummary(mPreference); + + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.account_sync_summary_all_on)); + } + + @Test + public void updateSummary_multipleSyncAdapters_shouldSetSummary() { + SyncAdapterType syncAdapterType1 = new SyncAdapterType("authority1" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType syncAdapterType2 = new SyncAdapterType("authority2" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType syncAdapterType3 = new SyncAdapterType("authority3" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType syncAdapterType4 = new SyncAdapterType("authority4" /* authority */, + "type1" /* accountType */, true /* userVisible */, true /* supportsUploading */); + SyncAdapterType[] syncAdapters = + {syncAdapterType1, syncAdapterType2, syncAdapterType3, syncAdapterType4}; + ShadowContentResolver.setSyncAdapterTypes(syncAdapters); + + ShadowContentResolver.setSyncAutomatically("authority4", false); + + mController.updateSummary(mPreference); + + assertThat(mPreference.getSummary()) + .isEqualTo(mContext.getString(R.string.account_sync_summary_some_on, 3, 4)); + } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContentResolver.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContentResolver.java index 36f170ab8e2..2e346a2e849 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContentResolver.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContentResolver.java @@ -16,6 +16,7 @@ package com.android.settings.testutils.shadow; +import android.accounts.Account; import android.content.ContentResolver; import android.content.SyncAdapterType; @@ -28,12 +29,20 @@ import org.robolectric.annotation.Implements; import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; +import java.util.HashMap; +import java.util.Map; + @Implements(ContentResolver.class) public class ShadowContentResolver { + private static SyncAdapterType[] sSyncAdapterTypes = new SyncAdapterType[0]; + private static Map sSyncable = new HashMap<>(); + private static Map sSyncAutomatically = new HashMap<>(); + private static Map sMasterSyncAutomatically = new HashMap<>(); + @Implementation public static SyncAdapterType[] getSyncAdapterTypesAsUser(int userId) { - return new SyncAdapterType[0]; + return sSyncAdapterTypes; } @Implementation @@ -44,4 +53,44 @@ public class ShadowContentResolver { .add(SearchIndexablesContract.NonIndexableKey.COLUMN_KEY_VALUE, ""); return cursor; } + + @Implementation + public static int getIsSyncableAsUser(Account account, String authority, int userId) { + return sSyncable.containsKey(authority) ? sSyncable.get(authority) : 1; + } + + @Implementation + public static boolean getSyncAutomaticallyAsUser(Account account, String authority, + int userId) { + return sSyncAutomatically.containsKey(authority) ? sSyncAutomatically.get(authority) : true; + } + + @Implementation + public static boolean getMasterSyncAutomaticallyAsUser(int userId) { + return sMasterSyncAutomatically.containsKey(userId) + ? sMasterSyncAutomatically.get(userId) : true; + } + + public static void setSyncAdapterTypes(SyncAdapterType[] syncAdapterTypes) { + sSyncAdapterTypes = syncAdapterTypes; + } + + public static void setSyncable(String authority, int syncable) { + sSyncable.put(authority, syncable); + } + + public static void setSyncAutomatically(String authority, boolean syncAutomatically) { + sSyncAutomatically.put(authority, syncAutomatically); + } + + public static void setMasterSyncAutomatically(int userId, boolean syncAutomatically) { + sMasterSyncAutomatically.put(userId, syncAutomatically); + } + + public static void reset() { + sSyncable.clear(); + sSyncAutomatically.clear(); + sMasterSyncAutomatically.clear(); + sSyncAdapterTypes = new SyncAdapterType[0]; + } }