Add automatic sync toggle

- Added option to accounts page that allows toggling of automatic
syncing
- Refactored some fragments to use SettingsFragment and controllers to
use new PreferenceController
- Small cleanup and additional test for AccountListPreferenceController
- Tests for new controller

Test: Tested on device
Test: New and existing tests pass
Bug: 73014882
Change-Id: Ibea3a14aaf5137a834888f8f04da36256b16f096
diff --git a/res/values/preference_keys.xml b/res/values/preference_keys.xml
index 6ceeb60..59ceba2 100644
--- a/res/values/preference_keys.xml
+++ b/res/values/preference_keys.xml
@@ -86,6 +86,7 @@
 
     <!-- Accounts -->
     <string name="pk_account_list" translatable="false">account_list</string>
+    <string name="pk_account_auto_sync" translatable="false">account_auto_sync</string>
     <string name="pk_add_account" translatable="false">add_account</string>
     <string name="pk_account_details_title" translatable="false">account_details_title</string>
     <string name="pk_account_details" translatable="false">account_details</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index e85f7f8..e95d291 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -561,6 +561,18 @@
     <string name="account_list_title">Accounts for
         <xliff:g id="current_user_name">%1$s</xliff:g>
     </string>
+    <!-- Switch label to enable auto sync account [CHAR LIMIT=60] -->
+    <string name="account_auto_sync_title">Automatically sync data</string>
+    <!-- Switch summary to enable auto sync account [CHAR LIMIT=60] -->
+    <string name="account_auto_sync_summary">Let apps refresh data automatically</string>
+    <!--  Title of dialog shown when user enables global auto sync [CHAR LIMIT=32] -->
+    <string name="data_usage_auto_sync_on_dialog_title">Turn auto-sync data on?</string>
+    <!--  Body of dialog shown when user enables global auto sync [CHAR LIMIT=NONE] -->
+    <string name="data_usage_auto_sync_on_dialog">Any changes you make to your accounts on the web will be automatically copied to your device.\n\nSome accounts may also automatically copy any changes you make on the phone to the web. A Google Account works this way.</string>
+    <!--  Title of dialog shown when user disables global auto sync [CHAR LIMIT=32] -->
+    <string name="data_usage_auto_sync_off_dialog_title">Turn auto-sync data off?</string>
+    <!--  Body of dialog shown when user disables global auto sync [CHAR LIMIT=NONE] -->
+    <string name="data_usage_auto_sync_off_dialog">This will conserve data, but you\u2019ll need to sync each account manually to collect recent information. And you won\u2019t receive notifications when updates occur.</string>
     <!-- Account details in Settings screen title [CHAR LIMIT=25] -->
     <string name="account_details_title">Account info</string>
     <!-- Account details in Settings screen title [CHAR LIMIT=25] -->
diff --git a/res/xml/account_settings_fragment.xml b/res/xml/account_settings_fragment.xml
index 8484f91..04dc8ee 100644
--- a/res/xml/account_settings_fragment.xml
+++ b/res/xml/account_settings_fragment.xml
@@ -23,4 +23,9 @@
         android:key="@string/pk_account_list"
         android:title="@string/account_list_title"
         settings:controller="com.android.car.settings.accounts.AccountListPreferenceController"/>
+    <SwitchPreference
+        android:key="@string/pk_account_auto_sync"
+        android:title="@string/account_auto_sync_title"
+        android:summary="@string/account_auto_sync_summary"
+        settings:controller="com.android.car.settings.accounts.AccountAutoSyncPreferenceController"/>
 </PreferenceScreen>
diff --git a/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceController.java b/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceController.java
new file mode 100644
index 0000000..e56d925
--- /dev/null
+++ b/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceController.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2018 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.car.settings.accounts;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.preference.TwoStatePreference;
+
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceController;
+
+/**
+ * Controller for the preference that allows the user to toggle automatic syncing of accounts.
+ *
+ * <p>Copied from {@link com.android.settings.users.AutoSyncDataPreferenceController}
+ */
+public class AccountAutoSyncPreferenceController extends
+        PreferenceController<TwoStatePreference> implements
+        ConfirmAutoSyncChangeDialogFragment.OnConfirmListener {
+    private static final String TAG_CONFIRM_AUTO_SYNC_CHANGE = "confirmAutoSyncChange";
+
+    private final UserHandle mUserHandle;
+
+    public AccountAutoSyncPreferenceController(Context context, String preferenceKey,
+            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+        super(context, preferenceKey, fragmentController, uxRestrictions);
+        CarUserManagerHelper carUserManagerHelper = new CarUserManagerHelper(context);
+        mUserHandle = carUserManagerHelper.getCurrentProcessUserInfo().getUserHandle();
+    }
+
+    @Override
+    protected Class<TwoStatePreference> getPreferenceType() {
+        return TwoStatePreference.class;
+    }
+
+    @Override
+    protected void updateState(TwoStatePreference preference) {
+        preference.setChecked(ContentResolver.getMasterSyncAutomaticallyAsUser(
+                mUserHandle.getIdentifier()));
+    }
+
+    @Override
+    protected void onCreateInternal() {
+        // If the dialog is still up, reattach the preference
+        ConfirmAutoSyncChangeDialogFragment dialogFragment =
+                (ConfirmAutoSyncChangeDialogFragment) getFragmentController().findDialogByTag(
+                        TAG_CONFIRM_AUTO_SYNC_CHANGE);
+        if (dialogFragment != null) {
+            dialogFragment.setOnConfirmListener(this);
+        }
+    }
+
+    @Override
+    protected boolean handlePreferenceChanged(TwoStatePreference preference, Object checked) {
+        getFragmentController().showDialog(
+                ConfirmAutoSyncChangeDialogFragment.newInstance((Boolean) checked, mUserHandle,
+                        this), TAG_CONFIRM_AUTO_SYNC_CHANGE);
+        // The dialog will change the state of the preference if the user confirms, so don't handle
+        // it here
+        return false;
+    }
+
+    @Override
+    public void onConfirm(boolean enabling) {
+        getPreference().setChecked(enabling);
+    }
+}
diff --git a/src/com/android/car/settings/accounts/AccountListPreferenceController.java b/src/com/android/car/settings/accounts/AccountListPreferenceController.java
index 04bf3ad..0f98700 100644
--- a/src/com/android/car/settings/accounts/AccountListPreferenceController.java
+++ b/src/com/android/car/settings/accounts/AccountListPreferenceController.java
@@ -18,6 +18,7 @@
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.car.drivingstate.CarUxRestrictions;
 import android.car.userlib.CarUserManagerHelper;
 import android.content.Context;
 import android.content.pm.UserInfo;
@@ -25,45 +26,42 @@
 import android.os.UserHandle;
 
 import androidx.collection.ArrayMap;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleObserver;
-import androidx.lifecycle.OnLifecycleEvent;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
-import androidx.preference.PreferenceScreen;
 
 import com.android.car.settings.R;
 import com.android.car.settings.common.FragmentController;
-import com.android.car.settings.common.NoSetupPreferenceController;
+import com.android.car.settings.common.PreferenceController;
 import com.android.settingslib.accounts.AuthenticatorHelper;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 
 /**
  * Controller for listing accounts.
  *
  * <p>Largely derived from {@link com.android.settings.accounts.AccountPreferenceController}
  */
-public class AccountListPreferenceController extends NoSetupPreferenceController implements
+public class AccountListPreferenceController extends
+        PreferenceController<PreferenceCategory> implements
         AuthenticatorHelper.OnAccountsUpdateListener,
-        CarUserManagerHelper.OnUsersUpdateListener, LifecycleObserver {
+        CarUserManagerHelper.OnUsersUpdateListener {
     private static final String NO_ACCOUNT_PREF_KEY = "no_accounts_added";
 
     private final UserInfo mUserInfo;
     private final CarUserManagerHelper mCarUserManagerHelper;
     private final ArrayMap<String, Preference> mPreferences = new ArrayMap<>();
     private AuthenticatorHelper mAuthenticatorHelper;
-    private PreferenceCategory mAccountsCategory;
     private String[] mAuthorities;
 
     public AccountListPreferenceController(Context context, String preferenceKey,
-            FragmentController fragmentController) {
-        super(context, preferenceKey, fragmentController);
+            FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
+        super(context, preferenceKey, fragmentController, uxRestrictions);
         mCarUserManagerHelper = new CarUserManagerHelper(context);
         mUserInfo = mCarUserManagerHelper.getCurrentProcessUserInfo();
         mAuthenticatorHelper = new AuthenticatorHelper(context,
@@ -76,30 +74,17 @@
     }
 
     @Override
-    public void displayPreference(PreferenceScreen screen) {
-        super.displayPreference(screen);
-
-        // Add preferences for each account if the controller should be available
-        if (isAvailable()) {
-            mAccountsCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey());
-            forceUpdateAccountsCategory();
-        }
+    protected Class<PreferenceCategory> getPreferenceType() {
+        return PreferenceCategory.class;
     }
 
     @Override
-    public boolean handlePreferenceTreeClick(Preference preference) {
-        // Show the account's details when an account is clicked on.
-        if (preference instanceof AccountPreference) {
-            AccountPreference accountPreference = (AccountPreference) preference;
-            getFragmentController().launchFragment(AccountDetailsFragment.newInstance(
-                    accountPreference.getAccount(), accountPreference.getLabel(), mUserInfo));
-            return true;
-        }
-        return false;
+    protected void updateState(PreferenceCategory preference) {
+        forceUpdateAccountsCategory();
     }
 
     @Override
-    public int getAvailabilityStatus() {
+    protected int getAvailabilityStatus() {
         return mCarUserManagerHelper.canCurrentProcessModifyAccounts() ? AVAILABLE
                 : DISABLED_FOR_USER;
     }
@@ -107,8 +92,8 @@
     /**
      * Registers the account update and user update callbacks.
      */
-    @OnLifecycleEvent(Lifecycle.Event.ON_START)
-    public void onStart() {
+    @Override
+    protected void onStartInternal() {
         mAuthenticatorHelper.listenToAccountUpdates();
         mCarUserManagerHelper.registerOnUsersUpdateListener(this);
     }
@@ -116,8 +101,8 @@
     /**
      * Unregisters the account update and user update callbacks.
      */
-    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
-    public void onStop() {
+    @Override
+    protected void onStopInternal() {
         mAuthenticatorHelper.stopListeningToAccountUpdates();
         mCarUserManagerHelper.unregisterOnUsersUpdateListener(this);
     }
@@ -134,33 +119,36 @@
         forceUpdateAccountsCategory();
     }
 
+    private boolean onAccountPreferenceClicked(AccountPreference preference) {
+        // Show the account's details when an account is clicked on.
+        getFragmentController().launchFragment(AccountDetailsFragment.newInstance(
+                preference.getAccount(), preference.getLabel(), mUserInfo));
+        return true;
+    }
+
     /** Forces a refresh of the account preferences. */
     private void forceUpdateAccountsCategory() {
         // Set the category title and include the user's name
-        mAccountsCategory.setTitle(mContext.getString(R.string.account_list_title, mUserInfo.name));
+        getPreference().setTitle(
+                getContext().getString(R.string.account_list_title, mUserInfo.name));
 
         // Recreate the authentication helper to refresh the list of enabled accounts
-        mAuthenticatorHelper = new AuthenticatorHelper(mContext, mUserInfo.getUserHandle(), this);
+        mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserInfo.getUserHandle(),
+                this);
 
-        Map<String, Preference> preferencesToRemove = new ArrayMap<>(mPreferences);
+        Set<String> preferencesToRemove = new HashSet<>(mPreferences.keySet());
         List<? extends Preference> preferences = getAccountPreferences(preferencesToRemove);
         // Add all preferences that aren't already shown. Manually set the order so that existing
         // preferences are reordered correctly.
         for (int i = 0; i < preferences.size(); i++) {
-            Preference preference = preferences.get(i);
-            String key = preference.getKey();
-            if (!mPreferences.containsKey(key)) {
-                preference.setOrder(i);
-                mAccountsCategory.addPreference(preference);
-                mPreferences.put(key, preference);
-            } else {
-                mPreferences.get(key).setOrder(i);
-            }
+            Preference pref = preferences.get(i);
+            pref.setOrder(i);
+            mPreferences.put(pref.getKey(), pref);
+            getPreference().addPreference(pref);
         }
 
-        // Remove all preferences that should no longer be shown
-        for (String key : preferencesToRemove.keySet()) {
-            mAccountsCategory.removePreference(mPreferences.get(key));
+        for (String key : preferencesToRemove) {
+            getPreference().removePreference(mPreferences.get(key));
             mPreferences.remove(key);
         }
     }
@@ -175,7 +163,7 @@
      *                            remain after method execution
      */
     private List<? extends Preference> getAccountPreferences(
-            Map<String, Preference> preferencesToRemove) {
+            Set<String> preferencesToRemove) {
         String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
         ArrayList<AccountPreference> accountPreferences =
                 new ArrayList<>(accountTypes.length);
@@ -186,34 +174,34 @@
             if (!accountTypeHasAnyRequestedAuthorities(accountType)) {
                 continue;
             }
-            CharSequence label = mAuthenticatorHelper.getLabelForType(mContext, accountType);
+            CharSequence label = mAuthenticatorHelper.getLabelForType(getContext(), accountType);
             if (label == null) {
                 continue;
             }
 
-            Account[] accounts = AccountManager.get(mContext)
+            Account[] accounts = AccountManager.get(getContext())
                     .getAccountsByTypeAsUser(accountType, mUserInfo.getUserHandle());
-            Drawable icon = mAuthenticatorHelper.getDrawableForType(mContext, accountType);
+            Drawable icon = mAuthenticatorHelper.getDrawableForType(getContext(), accountType);
 
             // Add a preference row for each individual account
             for (Account account : accounts) {
-                AccountPreference preference =
-                        (AccountPreference) preferencesToRemove.remove(
-                                AccountPreference.buildKey(account));
-                if (preference != null) {
-                    accountPreferences.add(preference);
-                    continue;
-                }
-                accountPreferences.add(new AccountPreference(
-                        mContext, account, label, icon));
+                String key = AccountPreference.buildKey(account);
+                AccountPreference preference = (AccountPreference) mPreferences.getOrDefault(key,
+                        new AccountPreference(getContext(), account, label, icon));
+                preference.setOnPreferenceClickListener(
+                        (Preference pref) -> onAccountPreferenceClicked((AccountPreference) pref));
+
+                accountPreferences.add(preference);
+                preferencesToRemove.remove(key);
             }
-            mAuthenticatorHelper.preloadDrawableForType(mContext, accountType);
+            mAuthenticatorHelper.preloadDrawableForType(getContext(), accountType);
         }
 
         // If there are no accounts, return the "no account added" preference.
         if (accountPreferences.isEmpty()) {
             preferencesToRemove.remove(NO_ACCOUNT_PREF_KEY);
-            return Arrays.asList(createNoAccountsAddedPreference());
+            return Arrays.asList(mPreferences.getOrDefault(NO_ACCOUNT_PREF_KEY,
+                    createNoAccountsAddedPreference()));
         }
 
         Collections.sort(accountPreferences, Comparator.comparing(
@@ -224,7 +212,7 @@
     }
 
     private Preference createNoAccountsAddedPreference() {
-        Preference emptyPreference = new Preference(mContext);
+        Preference emptyPreference = new Preference(getContext());
         emptyPreference.setTitle(R.string.no_accounts_added);
         emptyPreference.setKey(NO_ACCOUNT_PREF_KEY);
         emptyPreference.setSelectable(false);
diff --git a/src/com/android/car/settings/accounts/AccountSettingsFragment.java b/src/com/android/car/settings/accounts/AccountSettingsFragment.java
index e92f7f8..3c2a343 100644
--- a/src/com/android/car/settings/accounts/AccountSettingsFragment.java
+++ b/src/com/android/car/settings/accounts/AccountSettingsFragment.java
@@ -27,13 +27,13 @@
 import androidx.annotation.XmlRes;
 
 import com.android.car.settings.R;
-import com.android.car.settings.common.BasePreferenceFragment;
 import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.SettingsFragment;
 
 /**
  * Lists the user's accounts and any related options.
  */
-public class AccountSettingsFragment extends BasePreferenceFragment {
+public class AccountSettingsFragment extends SettingsFragment {
     @Override
     @XmlRes
     protected int getPreferenceScreenResId() {
diff --git a/src/com/android/car/settings/accounts/ConfirmAutoSyncChangeDialogFragment.java b/src/com/android/car/settings/accounts/ConfirmAutoSyncChangeDialogFragment.java
new file mode 100644
index 0000000..49b78d2
--- /dev/null
+++ b/src/com/android/car/settings/accounts/ConfirmAutoSyncChangeDialogFragment.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 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.car.settings.accounts;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import androidx.fragment.app.DialogFragment;
+
+import com.android.car.settings.R;
+
+/**
+ * Dialog to inform user about changing auto-sync setting.
+ */
+public class ConfirmAutoSyncChangeDialogFragment extends DialogFragment implements
+        DialogInterface.OnClickListener {
+    private static final String EXTRA_ENABLING = "enabling";
+    private static final String EXTRA_USER_HANDLE = "userHandle";
+    private boolean mEnabling;
+    private UserHandle mUserHandle;
+    private OnConfirmListener mListener;
+
+    /** Creates a new ConfirmAutoSyncChangeDialogFragment. */
+    public static ConfirmAutoSyncChangeDialogFragment newInstance(boolean enabling,
+            UserHandle userHandle, OnConfirmListener listener) {
+        ConfirmAutoSyncChangeDialogFragment fragment = new ConfirmAutoSyncChangeDialogFragment();
+
+        Bundle bundle = new Bundle();
+        bundle.putBoolean(EXTRA_ENABLING, enabling);
+        bundle.putParcelable(EXTRA_USER_HANDLE, userHandle);
+        fragment.setArguments(bundle);
+        fragment.setOnConfirmListener(listener);
+        return fragment;
+    }
+
+    /**
+     * Sets a OnConfirmListener that will get called if user confirms
+     * the dialog.
+     *
+     * @param listener Instance of {@link ConfirmAutoSyncChangeDialogFragment.OnConfirmListener} to
+     *                 call when confirmed.
+     */
+    public void setOnConfirmListener(OnConfirmListener listener) {
+        mListener = listener;
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        if (savedInstanceState != null) {
+            mEnabling = savedInstanceState.getBoolean(EXTRA_ENABLING);
+            mUserHandle = savedInstanceState.getParcelable(EXTRA_USER_HANDLE);
+        } else {
+            mEnabling = getArguments().getBoolean(EXTRA_ENABLING);
+            mUserHandle = getArguments().getParcelable(EXTRA_USER_HANDLE);
+        }
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+
+        if (mEnabling) {
+            builder.setTitle(R.string.data_usage_auto_sync_on_dialog_title);
+            builder.setMessage(R.string.data_usage_auto_sync_on_dialog);
+        } else {
+            builder.setTitle(R.string.data_usage_auto_sync_off_dialog_title);
+            builder.setMessage(R.string.data_usage_auto_sync_off_dialog);
+        }
+        builder.setPositiveButton(android.R.string.ok, this);
+        builder.setNegativeButton(android.R.string.cancel, null);
+        return builder.create();
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putBoolean(EXTRA_ENABLING, mEnabling);
+        outState.putParcelable(EXTRA_USER_HANDLE, mUserHandle);
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (which == DialogInterface.BUTTON_POSITIVE) {
+            ContentResolver.setMasterSyncAutomaticallyAsUser(mEnabling,
+                    mUserHandle.getIdentifier());
+            // Now that the user has confirmed, flip the switch
+            if (mListener != null) {
+                mListener.onConfirm(mEnabling);
+            }
+        }
+    }
+
+    /** Interface for receiving confirmation of an auto sync change. */
+    public interface OnConfirmListener {
+        /** Called when the user has confirmed an auto sync change. */
+        void onConfirm(boolean enabling);
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceControllerTest.java
new file mode 100644
index 0000000..08d9356
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/accounts/AccountAutoSyncPreferenceControllerTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2018 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.car.settings.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.car.userlib.CarUserManagerHelper;
+import android.content.ContentResolver;
+import android.content.pm.UserInfo;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.preference.SwitchPreference;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
+import com.android.car.settings.testutils.ShadowCarUserManagerHelper;
+import com.android.car.settings.testutils.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;
+
+/** Unit tests for {@link AccountAutoSyncPreferenceController}. */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowContentResolver.class, ShadowCarUserManagerHelper.class})
+public class AccountAutoSyncPreferenceControllerTest {
+    private static final int USER_ID = 0;
+    private PreferenceControllerTestHelper<AccountAutoSyncPreferenceController> mHelper;
+    private SwitchPreference mSwitchPreference;
+    private AccountAutoSyncPreferenceController mController;
+
+    @Mock
+    private CarUserManagerHelper mMockCarUserManagerHelper;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        ShadowCarUserManagerHelper.setMockInstance(mMockCarUserManagerHelper);
+        doReturn(new UserInfo(USER_ID, "name", 0)).when(
+                mMockCarUserManagerHelper).getCurrentProcessUserInfo();
+
+        mSwitchPreference = new SwitchPreference(application);
+        mHelper = new PreferenceControllerTestHelper<>(application,
+                AccountAutoSyncPreferenceController.class, mSwitchPreference);
+        mHelper.markState(Lifecycle.State.CREATED);
+        mController = mHelper.getController();
+    }
+
+    @After
+    public void tearDown() {
+        ShadowCarUserManagerHelper.reset();
+    }
+
+    @Test
+    public void refreshUi_masterSyncOn_preferenceShouldBeChecked() {
+        ContentResolver.setMasterSyncAutomaticallyAsUser(true, USER_ID);
+
+        mController.refreshUi();
+
+        assertThat(mSwitchPreference.isChecked()).isTrue();
+    }
+
+    @Test
+    public void refreshUi_masterSyncOff_preferenceShouldNotBeChecked() {
+        ContentResolver.setMasterSyncAutomaticallyAsUser(false, USER_ID);
+
+        mController.refreshUi();
+
+        assertThat(mSwitchPreference.isChecked()).isFalse();
+    }
+
+    @Test
+    public void onPreferenceClicked_shouldOpenDialog() {
+        mSwitchPreference.performClick();
+
+        verify(mHelper.getMockFragmentController()).showDialog(
+                any(ConfirmAutoSyncChangeDialogFragment.class), eq("confirmAutoSyncChange"));
+    }
+
+    @Test
+    public void onConfirm_shouldTogglePreference() {
+        // Set the preference as unchecked first so that the state is known
+        ContentResolver.setMasterSyncAutomaticallyAsUser(false, USER_ID);
+        mController.refreshUi();
+        assertThat(mSwitchPreference.isChecked()).isFalse();
+
+        mController.onConfirm(true);
+        assertThat(mSwitchPreference.isChecked()).isTrue();
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/accounts/AccountListPreferenceControllerTest.java b/tests/robotests/src/com/android/car/settings/accounts/AccountListPreferenceControllerTest.java
index 519fa51..9da11e4 100644
--- a/tests/robotests/src/com/android/car/settings/accounts/AccountListPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/car/settings/accounts/AccountListPreferenceControllerTest.java
@@ -18,7 +18,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
 import static org.robolectric.RuntimeEnvironment.application;
 
 import android.accounts.Account;
@@ -28,14 +30,14 @@
 import android.content.pm.UserInfo;
 import android.os.UserHandle;
 
+import androidx.lifecycle.Lifecycle;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
-import androidx.preference.PreferenceManager;
-import androidx.preference.PreferenceScreen;
 
 import com.android.car.settings.CarSettingsRobolectricTestRunner;
 import com.android.car.settings.R;
 import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.PreferenceControllerTestHelper;
 import com.android.car.settings.testutils.ShadowAccountManager;
 import com.android.car.settings.testutils.ShadowCarUserManagerHelper;
 import com.android.car.settings.testutils.ShadowContentResolver;
@@ -54,21 +56,18 @@
 @Config(shadows = {ShadowCarUserManagerHelper.class, ShadowContentResolver.class,
         ShadowAccountManager.class})
 public class AccountListPreferenceControllerTest {
-    private static final String PREFERENCE_KEY = "test_key";
     private static final int USER_ID = 0;
     private static final String USER_NAME = "name";
     private static final int NOT_THIS_USER_ID = 1;
-
-    private PreferenceScreen mPreferenceScreen;
+    private PreferenceControllerTestHelper<AccountListPreferenceController> mHelper;
+    private AccountManager mAccountManager = AccountManager.get(application);
     private PreferenceCategory mPreferenceCategory;
     private AccountListPreferenceController mController;
-    @Mock
     private FragmentController mFragmentController;
+
     @Mock
     private CarUserManagerHelper mMockCarUserManagerHelper;
 
-    private AccountManager mAccountManager = AccountManager.get(application);
-
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -84,14 +83,12 @@
         addAuthenticator(/* type= */ "com.acct1", /* labelRes= */ R.string.account_type1_label);
         addAuthenticator(/* type= */ "com.acct2", /* labelRes= */ R.string.account_type2_label);
 
-        // Add the category for the preference controller
         mPreferenceCategory = new PreferenceCategory(application);
-        mPreferenceCategory.setKey(PREFERENCE_KEY);
-        mPreferenceScreen = new PreferenceManager(application).createPreferenceScreen(application);
-        mPreferenceScreen.addPreference(mPreferenceCategory);
-
-        mController = new AccountListPreferenceController(application, PREFERENCE_KEY,
-                mFragmentController);
+        mHelper = new PreferenceControllerTestHelper<>(application,
+                AccountListPreferenceController.class, mPreferenceCategory);
+        mHelper.markState(Lifecycle.State.CREATED);
+        mController = mHelper.getController();
+        mFragmentController = mHelper.getMockFragmentController();
     }
 
     @After
@@ -101,16 +98,14 @@
     }
 
     @Test
-    public void displayPreference_preferenceCategoryTitleShouldBeSet() {
-        mController.displayPreference(mPreferenceScreen);
-
+    public void onCreate_preferenceCategoryTitleShouldBeSet() {
         String expectedTitle = application.getString(R.string.account_list_title, "name");
         assertThat(mPreferenceCategory.getTitle()).isEqualTo(expectedTitle);
     }
 
     @Test
-    public void displayPreference_hasNoAccounts_shouldDisplayNoAccountPref() {
-        mController.displayPreference(mPreferenceScreen);
+    public void refreshUi_hasNoAccounts_shouldDisplayNoAccountPref() {
+        mController.refreshUi();
 
         assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(1);
         Preference noAccountPref = mPreferenceCategory.getPreference(0);
@@ -120,11 +115,11 @@
     }
 
     @Test
-    public void displayPreference_hasAccounts_shouldDisplayAccounts() {
+    public void refreshUi_hasAccounts_shouldDisplayAccounts() {
         addAccount(/* name= */ "Account1", /* type= */ "com.acct1");
         addAccount(/* name= */ "Account2", /* type= */ "com.acct2");
 
-        mController.displayPreference(mPreferenceScreen);
+        mController.refreshUi();
 
         assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2);
 
@@ -138,14 +133,14 @@
     }
 
     @Test
-    public void displayPreference_hasUnauthenticatedAccount_shouldNotDisplayAccount() {
+    public void refreshUi_hasUnauthenticatedAccount_shouldNotDisplayAccount() {
         addAccount(/* name= */ "Account1", /* type= */ "com.acct1");
         addAccount(/* name= */ "Account2", /* type= */ "com.acct2");
         // There is not authenticator for account type "com.acct3" so this account should not
         // appear in the list of displayed accounts.
         addAccount(/* name= */ "Account3", /* type= */ "com.acct3");
 
-        mController.displayPreference(mPreferenceScreen);
+        mController.refreshUi();
 
         assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2);
 
@@ -163,7 +158,7 @@
         addAccount(/* name= */ "Account1", /* type= */ "com.acct1");
         addAccount(/* name= */ "Account2", /* type= */ "com.acct2");
 
-        mController.displayPreference(mPreferenceScreen);
+        mController.refreshUi();
         assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2);
 
         getShadowAccountManager().removeAllAccounts();
@@ -182,7 +177,7 @@
         addAccount(/* name= */ "Account1", /* type= */ "com.acct1");
         addAccount(/* name= */ "Account2", /* type= */ "com.acct2");
 
-        mController.displayPreference(mPreferenceScreen);
+        mController.refreshUi();
         assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2);
 
         getShadowAccountManager().removeAllAccounts();
@@ -198,7 +193,7 @@
         addAccount(/* name= */ "Account1", /* type= */ "com.acct1");
         addAccount(/* name= */ "Account2", /* type= */ "com.acct2");
 
-        mController.displayPreference(mPreferenceScreen);
+        mController.refreshUi();
         assertThat(mPreferenceCategory.getPreferenceCount()).isEqualTo(2);
 
         getShadowAccountManager().removeAllAccounts();
@@ -212,6 +207,17 @@
         assertThat(firstPref.getSummary()).isEqualTo("Type 1");
     }
 
+    @Test
+    public void onAccountPreferenceClicked_shouldLaunchAccountDetailsFragment() {
+        addAccount(/* name= */ "Account1", /* type= */ "com.acct1");
+        mController.refreshUi();
+
+        Preference firstPref = mPreferenceCategory.getPreference(0);
+        firstPref.performClick();
+
+        verify(mFragmentController).launchFragment(any(AccountDetailsFragment.class));
+    }
+
     private void addAccount(String name, String type) {
         getShadowAccountManager().addAccount(new Account(name, type));
     }
diff --git a/tests/robotests/src/com/android/car/settings/accounts/ConfirmAutoSyncChangeDialogFragmentTest.java b/tests/robotests/src/com/android/car/settings/accounts/ConfirmAutoSyncChangeDialogFragmentTest.java
new file mode 100644
index 0000000..8bcb191
--- /dev/null
+++ b/tests/robotests/src/com/android/car/settings/accounts/ConfirmAutoSyncChangeDialogFragmentTest.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 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.car.settings.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.DialogInterface;
+import android.os.UserHandle;
+
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.car.settings.CarSettingsRobolectricTestRunner;
+import com.android.car.settings.R;
+import com.android.car.settings.accounts.ConfirmAutoSyncChangeDialogFragment.OnConfirmListener;
+import com.android.car.settings.testutils.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.Robolectric;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+
+/**
+ * Tests for the {@link ConfirmAutoSyncChangeDialogFragment}.
+ */
+@RunWith(CarSettingsRobolectricTestRunner.class)
+@Config(shadows = {ShadowContentResolver.class})
+public class ConfirmAutoSyncChangeDialogFragmentTest {
+    private static final int USER_ID = 0;
+    private static final UserHandle USER_HANDLE = new UserHandle(USER_ID);
+
+    private TestDialogActivity mActivity;
+    @Mock
+    private OnConfirmListener mMockListener;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mActivity = Robolectric.setupActivity(TestDialogActivity.class);
+    }
+
+    @After
+    public void tearDown() {
+        ShadowContentResolver.reset();
+    }
+
+    @Test
+    public void isEnabling_shouldSetCorrectTitle() {
+        mActivity.showDialog(/* enabling= */ true, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        assertThat(Shadows.shadowOf(dialog).getTitle()).isEqualTo(
+                application.getString(R.string.data_usage_auto_sync_on_dialog_title));
+    }
+
+    @Test
+    public void isDisabling_shouldSetCorrectTitle() {
+        mActivity.showDialog(/* enabling= */ false, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        assertThat(Shadows.shadowOf(dialog).getTitle()).isEqualTo(
+                application.getString(R.string.data_usage_auto_sync_off_dialog_title));
+    }
+
+    @Test
+    public void isEnabling_shouldSetCorrectMessage() {
+        mActivity.showDialog(/* enabling= */ true, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        assertThat(Shadows.shadowOf(dialog).getMessage()).isEqualTo(
+                application.getString(R.string.data_usage_auto_sync_on_dialog));
+    }
+
+    @Test
+    public void isDisabling_shouldSetCorrectMessage() {
+        mActivity.showDialog(/* enabling= */ false, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        assertThat(Shadows.shadowOf(dialog).getMessage()).isEqualTo(
+                application.getString(R.string.data_usage_auto_sync_off_dialog));
+    }
+
+    @Test
+    public void onButtonClick_isEnabling_shouldTurnOnMasterSyncAutomatically() {
+        mActivity.showDialog(/* enabling= */ true, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+        assertThat(ContentResolver.getMasterSyncAutomaticallyAsUser(USER_ID)).isTrue();
+    }
+
+    @Test
+    public void onButtonClick_isDisabling_shouldTurnOffMasterSyncAutomatically() {
+        mActivity.showDialog(/* enabling= */ false, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+        assertThat(ContentResolver.getMasterSyncAutomaticallyAsUser(USER_ID)).isFalse();
+    }
+
+    @Test
+    public void onButtonClick_isEnabling_shouldCallOnConfirmWithEnable() {
+        mActivity.showDialog(/* enabling= */ true, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+        verify(mMockListener).onConfirm(true);
+    }
+
+    @Test
+    public void onButtonClick_isDisabling_shouldCallOnConfirmWithDisable() {
+        mActivity.showDialog(/* enabling= */ false, mMockListener);
+        AlertDialog dialog = (AlertDialog) mActivity.getDialog().getDialog();
+
+        dialog.getButton(DialogInterface.BUTTON_POSITIVE).performClick();
+
+        verify(mMockListener).onConfirm(false);
+    }
+
+    public static class TestDialogActivity extends FragmentActivity {
+        private static final String DIALOG_TAG = "dialog";
+
+        private void showDialog(boolean enabling, OnConfirmListener listener) {
+            ConfirmAutoSyncChangeDialogFragment dialogFragment =
+                    ConfirmAutoSyncChangeDialogFragment.newInstance(enabling, USER_HANDLE,
+                            listener);
+            dialogFragment.show(getSupportFragmentManager(), DIALOG_TAG);
+        }
+
+        private ConfirmAutoSyncChangeDialogFragment getDialog() {
+            return (ConfirmAutoSyncChangeDialogFragment) getSupportFragmentManager()
+                    .findFragmentByTag(DIALOG_TAG);
+        }
+    }
+}
diff --git a/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java b/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java
index 5535c0c..c45064e 100644
--- a/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java
+++ b/tests/robotests/src/com/android/car/settings/testutils/ShadowContentResolver.java
@@ -25,6 +25,7 @@
 
 import org.robolectric.annotation.Implementation;
 import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -109,6 +110,7 @@
         sSyncStatus.put(authority, status);
     }
 
+    @Resetter
     public static void reset() {
         org.robolectric.shadows.ShadowContentResolver.reset();
         sSyncable.clear();