Merge "Add a flag for the restore assistant" into ub-contactsdialer-h-dev
diff --git a/res/layout/contact_list_content.xml b/res/layout/contact_list_content.xml
index 8420b26..39f669f 100644
--- a/res/layout/contact_list_content.xml
+++ b/res/layout/contact_list_content.xml
@@ -28,6 +28,37 @@
     android:visibility="gone"
     android:background="?attr/contact_browser_background" >
 
+    <LinearLayout
+        android:id="@+id/alert_container"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:background="@color/alert_background"
+        android:paddingStart="20dp"
+        android:visibility="gone">
+
+        <TextView
+            android:id="@+id/alert_text"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:paddingTop="16dp"
+            android:paddingBottom="16dp"
+            android:layout_weight="1"
+            android:textColor="@android:color/black"
+            android:textSize="16sp"/>
+
+        <ImageView
+            android:id="@+id/alert_dismiss_icon"
+            android:layout_width="56dp"
+            android:layout_height="match_parent"
+            android:layout_gravity="center_vertical"
+            android:contentDescription="@string/dismiss_sync_alert"
+            android:background="?android:attr/selectableItemBackground"
+            android:scaleType="center"
+            android:src="@drawable/ic_cancel_black_24dp" />
+    </LinearLayout>
+
     <!-- Shown only when an Account filter is set.
          - paddingTop should be here to show "shade" effect correctly. -->
     <include layout="@layout/account_filter_header" />
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 98c522d..e2024fd 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -260,4 +260,7 @@
 
     <!-- tint color for device account icons -->
     <color name="device_account_tint_color">#7f7f7f</color>
+
+    <!-- Background color for sync-off alert. -->
+    <color name="alert_background">#e0e0e0</color>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2c911c9..1ef1bd5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1573,12 +1573,6 @@
     <!--Label of the "default account" setting option to set default editor account. [CHAR LIMIT=80]-->
     <string name="default_editor_account">Default account for new contacts</string>
 
-    <!--Label of the "Sync contact metadata" setting option to set sync account for Lychee. [CHAR LIMIT=80]-->
-    <string name="sync_contact_metadata_title">Sync contact metadata [DOGFOOD]</string>
-
-    <!--Label of the "Sync contact metadata" setting dialog to set sync account for Lychee. [CHAR LIMIT=80]-->
-    <string name="sync_contact_metadata_dialog_title">Sync contact metadata</string>
-
     <!-- Title of my info preference, showing the name of user's personal profile [CHAR LIMIT=30]-->
     <string name="settings_my_info_title">My info</string>
 
@@ -1819,4 +1813,24 @@
     <!-- Content description of the cancel navigation icon shown in SIM import dialog toolbar -->
     <string name="sim_import_cancel_content_description">Cancel import</string>
 
+    <!-- Alert for letting user know that their device auto-sync setting is turned off,
+         in case they are wondering why they are not seeing any contact. [CHAR LIMIT=150] -->
+    <string name="auto_sync_off">Auto-sync is off. Tap to turn on.</string>
+
+    <!-- Content description for the "X" image icon for dismissing an alert.[CHAR LIMIT=50] -->
+    <string name="dismiss_sync_alert">Dismiss</string>
+
+    <!-- Alert for letting user know that their account level sync setting is turned off,
+         in case they are wondering why they are not seeing any contact. [CHAR LIMIT=150] -->
+    <string name="account_sync_off">Account sync is off. Tap to turn on.</string>
+
+    <!-- Title of dialog to turn auto-sync on [CHAR LIMIT=100] -->
+    <string name="turn_auto_sync_on_dialog_title">Turn auto-sync on?</string>
+
+    <!-- Text of dialog to turn auto-sync on [CHAR LIMIT=500] -->
+    <string name="turn_auto_sync_on_dialog_body">Changes you make to all apps and accounts,
+        not just Contacts, will be synchronized between the web and your devices.</string>
+
+    <!-- Confirm button text for dialog to turn auto-sync on [CHAR LIMIT=30] -->
+    <string name="turn_auto_sync_on_dialog_confirm_btn">Turn on</string>
 </resources>
diff --git a/src-bind/com/android/contactsbind/ObjectFactory.java b/src-bind/com/android/contactsbind/ObjectFactory.java
index c9539ee..d6799e2 100644
--- a/src-bind/com/android/contactsbind/ObjectFactory.java
+++ b/src-bind/com/android/contactsbind/ObjectFactory.java
@@ -14,7 +14,6 @@
 package com.android.contactsbind;
 
 import com.android.contacts.common.logging.Logger;
-import com.android.contacts.common.preference.PreferenceManager;
 import com.android.contactsbind.search.AutocompleteHelper;
 import com.android.contacts.common.util.DeviceLocalAccountTypeFactory;
 
@@ -32,8 +31,6 @@
         return null;
     }
 
-    public static PreferenceManager getPreferenceManager(Context context) { return null; }
-
     public static AutocompleteHelper getAutocompleteHelper(Context context) {
         return null;
     }
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 9326b0d..0d4b4c0 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -173,9 +173,7 @@
             // the syncs is in progress.
             if (syncableAccounts != null && syncableAccounts.size() > 0) {
                 for (Account account: syncableAccounts) {
-                    if (SyncUtil.isSyncStatusPendingOrActive(account)
-                            || SyncUtil.isUnsyncableGoogleAccount(account)) {
-                        swipeRefreshLayout.setRefreshing(true);
+                    if (SyncUtil.isSyncStatusPendingOrActive(account)) {
                         return;
                     }
                 }
diff --git a/src/com/android/contacts/common/Experiments.java b/src/com/android/contacts/common/Experiments.java
index a8120f6..5de4d5d 100644
--- a/src/com/android/contacts/common/Experiments.java
+++ b/src/com/android/contacts/common/Experiments.java
@@ -58,6 +58,12 @@
     public static final String PULL_TO_REFRESH = "PullToRefresh__pull_to_refresh";
 
     /**
+     * Flags for maximum time to show spinner for a contacts sync.
+     */
+    public static final String PULL_TO_REFRESH_CANCEL_REFRESH_MILLIS =
+            "PullToRefresh__cancel_refresh_millis";
+
+    /**
      * Search study boolean indicating whether to inject yenta search results before CP2 results.
      */
     public static final String SEARCH_YENTA = "Search__yenta";
diff --git a/src/com/android/contacts/common/compat/MetadataSyncEnabledCompat.java b/src/com/android/contacts/common/compat/MetadataSyncEnabledCompat.java
deleted file mode 100644
index 4a9650f..0000000
--- a/src/com/android/contacts/common/compat/MetadataSyncEnabledCompat.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright (C) 2016 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.contacts.common.compat;
-
-import android.content.Context;
-import android.provider.Settings;
-
-public class MetadataSyncEnabledCompat {
-    public static boolean isMetadataSyncEnabled(Context context) {
-        return CompatUtils.isNCompatible()
-                ? (Settings.Global.getInt(context.getContentResolver(),
-                        Settings.Global.CONTACT_METADATA_SYNC_ENABLED, 0) == 1)
-                : false;
-    }
-}
\ No newline at end of file
diff --git a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
index b961b67..157d0a8 100644
--- a/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
+++ b/src/com/android/contacts/common/list/CustomContactListFilterActivity.java
@@ -143,7 +143,10 @@
 
             final AccountSet accounts = new AccountSet();
 
-            final List<AccountWithDataSet> sourceAccounts = accountTypes.getAccounts(false);
+            // Don't include the null account because it doesn't support writing to
+            // ContactsContract.Settings
+            final List<AccountWithDataSet> sourceAccounts = accountTypes.getAccounts(
+                    AccountTypeManager.nonNullAccountFilter());
             final AccountDisplayInfoFactory displayableAccountFactory =
                     new AccountDisplayInfoFactory(context, sourceAccounts);
             for (AccountWithDataSet account : sourceAccounts) {
diff --git a/src/com/android/contacts/common/model/AccountTypeManager.java b/src/com/android/contacts/common/model/AccountTypeManager.java
index 705f5bf..d91e73b 100644
--- a/src/com/android/contacts/common/model/AccountTypeManager.java
+++ b/src/com/android/contacts/common/model/AccountTypeManager.java
@@ -61,6 +61,8 @@
 import com.android.contactsbind.ObjectFactory;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
@@ -76,6 +78,8 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import javax.annotation.Nullable;
+
 import static com.android.contacts.common.util.DeviceLocalAccountTypeFactory.Util.isLocalAccountType;
 
 /**
@@ -128,6 +132,11 @@
         }
 
         @Override
+        public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
+            return Collections.emptyList();
+        }
+
+        @Override
         public List<AccountWithDataSet> getGroupWritableAccounts() {
             return Collections.emptyList();
         }
@@ -166,6 +175,8 @@
     // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
     public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
 
+    public abstract List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter);
+
     public abstract List<AccountWithDataSet> getSortedAccounts(AccountWithDataSet defaultAccount,
             boolean contactWritableOnly);
 
@@ -272,6 +283,14 @@
         return canGetAccounts && canReadContacts;
     }
 
+    public static Predicate<AccountWithDataSet> nonNullAccountFilter() {
+        return new Predicate<AccountWithDataSet>() {
+            @Override
+            public boolean apply(@Nullable AccountWithDataSet account) {
+                return account != null && account.name != null && account.type != null;
+            }
+        };
+    }
 }
 
 class AccountComparator implements Comparator<AccountWithDataSet> {
@@ -755,6 +774,11 @@
         return Lists.newArrayList(contactWritableOnly ? mContactWritableAccounts : mAccounts);
     }
 
+    @Override
+    public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
+        return new ArrayList<>(Collections2.filter(mAccounts, filter));
+    }
+
     /**
      * Return list of all known or contact writable {@link AccountWithDataSet}'s sorted by
      * {@code defaultAccount}.
diff --git a/src/com/android/contacts/common/preference/ContactsPreferences.java b/src/com/android/contacts/common/preference/ContactsPreferences.java
index 26073f4..8865e91 100644
--- a/src/com/android/contacts/common/preference/ContactsPreferences.java
+++ b/src/com/android/contacts/common/preference/ContactsPreferences.java
@@ -16,7 +16,6 @@
 
 package com.android.contacts.common.preference;
 
-import android.accounts.Account;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -27,7 +26,6 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.preference.PreferenceManager;
-import android.provider.ContactsContract;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
 import android.support.annotation.NonNull;
@@ -36,10 +34,7 @@
 
 import com.android.contacts.common.R;
 import com.android.contacts.common.model.account.AccountWithDataSet;
-import com.android.contacts.common.model.account.GoogleAccountType;
-import com.android.contacts.common.model.AccountTypeManager;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -75,14 +70,6 @@
 
     public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
 
-    public static final String DO_NOT_SYNC_CONTACT_METADATA_MSG = "Do not sync metadata";
-
-    public static final String CONTACT_METADATA_AUTHORITY = "com.android.contacts.metadata";
-
-    public static final String SHOULD_CLEAR_METADATA_BEFORE_SYNCING =
-            "should_clear_metadata_before_syncing";
-
-    public static final String ONLY_CLEAR_DONOT_SYNC = "only_clear_donot_sync";
     /**
      * Value to use when a preference is unassigned and needs to be read from the shared preferences
      */
@@ -248,74 +235,6 @@
         return false;
     }
 
-    public String getContactMetadataSyncAccountName() {
-        final Account syncAccount = getContactMetadataSyncAccount();
-        return syncAccount == null ? DO_NOT_SYNC_CONTACT_METADATA_MSG : syncAccount.name;
-    }
-
-    public void setContactMetadataSyncAccount(AccountWithDataSet accountWithDataSet) {
-        final String mContactMetadataSyncAccount =
-                accountWithDataSet == null ? null : accountWithDataSet.name;
-        requestMetadataSyncForAccount(mContactMetadataSyncAccount);
-    }
-
-    private Account getContactMetadataSyncAccount() {
-        for (Account account : getFocusGoogleAccounts()) {
-            if (ContentResolver.getIsSyncable(account, CONTACT_METADATA_AUTHORITY) == 1
-                    && ContentResolver.getSyncAutomatically(account, CONTACT_METADATA_AUTHORITY)) {
-                return account;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Turn on contact metadata sync for this {@param accountName} and turn off automatic sync
-     * for other accounts. If accountName is null, then turn off automatic sync for all accounts.
-     */
-    private void requestMetadataSyncForAccount(String accountName) {
-        for (Account account : getFocusGoogleAccounts()) {
-            if (!TextUtils.isEmpty(accountName) && accountName.equals(account.name)) {
-                // Request sync.
-                final Bundle b = new Bundle();
-                b.putBoolean(SHOULD_CLEAR_METADATA_BEFORE_SYNCING, true);
-                b.putBoolean(ONLY_CLEAR_DONOT_SYNC, false);
-                b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
-                b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
-                ContentResolver.requestSync(account, CONTACT_METADATA_AUTHORITY, b);
-
-                ContentResolver.setSyncAutomatically(account, CONTACT_METADATA_AUTHORITY, true);
-            } else if (ContentResolver.getSyncAutomatically(account, CONTACT_METADATA_AUTHORITY)) {
-                // Turn off automatic sync for previous sync account.
-                ContentResolver.setSyncAutomatically(account, CONTACT_METADATA_AUTHORITY, false);
-                if (TextUtils.isEmpty(accountName)) {
-                    // Request sync to clear old data.
-                    final Bundle b = new Bundle();
-                    b.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
-                    b.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
-                    b.putBoolean(SHOULD_CLEAR_METADATA_BEFORE_SYNCING, true);
-                    b.putBoolean(ONLY_CLEAR_DONOT_SYNC, true);
-                    ContentResolver.requestSync(account, CONTACT_METADATA_AUTHORITY, b);
-                }
-            }
-        }
-    }
-
-    /**
-     * @return google accounts with "com.google" account type and null data set.
-     */
-    private List<Account> getFocusGoogleAccounts() {
-        List<Account> focusGoogleAccounts = new ArrayList<Account>();
-        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
-        List<AccountWithDataSet> accounts = accountTypeManager.getAccounts(true);
-        for (AccountWithDataSet account : accounts) {
-            if (GoogleAccountType.ACCOUNT_TYPE.equals(account.type) && account.dataSet == null) {
-                focusGoogleAccounts.add(account.getAccountOrNull());
-            }
-        }
-        return focusGoogleAccounts;
-    }
-
     public void registerChangeListener(ChangeListener listener) {
         if (mListener != null) unregisterChangeListener();
 
diff --git a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
index b3b2832..a6881ed 100644
--- a/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
+++ b/src/com/android/contacts/common/preference/DisplayOptionsPreferenceFragment.java
@@ -57,7 +57,6 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.util.AccountFilterUtil;
 import com.android.contacts.common.util.ImplicitIntentsUtil;
-import com.android.contactsbind.ObjectFactory;
 
 import java.util.List;
 
@@ -207,7 +206,6 @@
         mAreContactsAvailable = args.getBoolean(ARG_CONTACTS_AVAILABLE);
 
         removeUnsupportedPreferences();
-        addExtraPreferences();
 
         mMyInfoPreference = findPreference(KEY_MY_INFO);
 
@@ -291,16 +289,6 @@
         }
     }
 
-    private void addExtraPreferences() {
-        final PreferenceManager preferenceManager = ObjectFactory.getPreferenceManager(
-                getContext());
-        if (preferenceManager != null) {
-            for (Preference preference : preferenceManager.getPreferences()) {
-                getPreferenceScreen().addPreference(preference);
-            }
-        }
-    }
-
     @Override
     public Context getContext() {
         return getActivity();
diff --git a/src/com/android/contacts/common/preference/PreferenceManager.java b/src/com/android/contacts/common/preference/PreferenceManager.java
deleted file mode 100644
index 816f94e..0000000
--- a/src/com/android/contacts/common/preference/PreferenceManager.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2016 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.contacts.common.preference;
-
-import android.preference.Preference;
-
-import java.util.List;
-
-public interface PreferenceManager {
-    List<Preference> getPreferences();
-}
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
index c8da194..bf4f891 100644
--- a/src/com/android/contacts/list/ContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -92,7 +92,7 @@
     private boolean mSelectionVerified;
     private int mLastSelectedPosition = -1;
     private boolean mRefreshingContactUri;
-    protected ContactListFilter mFilter;
+    private ContactListFilter mFilter;
     private String mPersistentSelectionPrefix = PERSISTENT_SELECTION_PREFIX;
 
     protected OnContactBrowserActionListener mListener;
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 25315e8..a748c06 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -26,10 +26,12 @@
 import android.content.Loader;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Directory;
 import android.support.v4.widget.SwipeRefreshLayout;
@@ -88,7 +90,8 @@
  * Fragment containing a contact list used for browsing (as compared to
  * picking a contact with one of the PICK intents).
  */
-public class DefaultContactBrowseListFragment extends ContactBrowseListFragment {
+public class DefaultContactBrowseListFragment extends ContactBrowseListFragment
+        implements EnableGlobalSyncDialogFragment.Listener {
 
     private static final String TAG = "DefaultListFragment";
     private static final String ENABLE_DEBUG_OPTIONS_HIDDEN_CODE = "debug debug!";
@@ -102,7 +105,22 @@
     private View mEmptyHomeView;
     private View mAccountFilterContainer;
     private TextView mSearchProgressText;
+
     private SwipeRefreshLayout mSwipeRefreshLayout;
+    private final Handler mHandler = new Handler();
+    private final Runnable mCancelRefresh = new Runnable() {
+        @Override
+        public void run() {
+            if (mSwipeRefreshLayout.isRefreshing()) {
+                mSwipeRefreshLayout.setRefreshing(false);
+            }
+        }
+    };
+
+    private View mAlertContainer;
+    private TextView mAlertText;
+    private ImageView mAlertDismissIcon;
+    private int mReasonSyncOff = SyncUtil.SYNC_SETTING_SYNC_ON;
 
     private boolean mContactsAvailable;
     private boolean mEnableDebugMenuOptions;
@@ -164,6 +182,10 @@
                     maybeHideCheckBoxes();
                     mActivity.invalidateOptionsMenu();
                     mActivity.showFabWithAnimation(/* showFab */ true);
+
+                    // Alert user if sync is off and not dismissed before
+                    setSyncOffAlert();
+
                     // Determine whether the account has pullToRefresh feature
                     if (Flags.getInstance(getContext()).getBoolean(Experiments.PULL_TO_REFRESH)) {
                         setSwipeRefreshLayoutEnabledOrNot(getFilter());
@@ -452,6 +474,66 @@
 
         mSearchProgress = getView().findViewById(R.id.search_progress);
         mSearchProgressText = (TextView) mSearchHeaderView.findViewById(R.id.totalContactsText);
+
+        mAlertContainer = getView().findViewById(R.id.alert_container);
+        mAlertText = (TextView) mAlertContainer.findViewById(R.id.alert_text);
+        mAlertDismissIcon = (ImageView) mAlertContainer.findViewById(R.id.alert_dismiss_icon);
+        mAlertText.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                turnSyncOn();
+            }
+        });
+        mAlertDismissIcon.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                dismiss();
+            }
+        });
+
+        mAlertContainer.setVisibility(View.GONE);
+    }
+
+    private void turnSyncOn() {
+        final ContactListFilter filter = getFilter();
+        if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT
+                && mReasonSyncOff == SyncUtil.SYNC_SETTING_ACCOUNT_SYNC_OFF) {
+            ContentResolver.setSyncAutomatically(
+                    new Account(filter.accountName, filter.accountType),
+                    ContactsContract.AUTHORITY, true);
+            mAlertContainer.setVisibility(View.GONE);
+        } else {
+            final EnableGlobalSyncDialogFragment dialog = new
+                    EnableGlobalSyncDialogFragment();
+            dialog.show(this, filter);
+        }
+    }
+
+    @Override
+    public void onEnableAutoSync(ContactListFilter filter) {
+        // Turn on auto-sync
+        ContentResolver.setMasterSyncAutomatically(true);
+        // Also enable Contacts sync
+        final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(
+                getContext()).getAccounts(/* contactsWritableOnly */ true);
+        final List<Account> syncableAccounts = filter.getSyncableAccounts(accounts);
+        if (syncableAccounts != null && syncableAccounts.size() > 0) {
+            for (Account account : syncableAccounts) {
+                ContentResolver.setSyncAutomatically(new Account(account.name, account.type),
+                        ContactsContract.AUTHORITY, true);
+            }
+        }
+        mAlertContainer.setVisibility(View.GONE);
+    }
+
+    private void dismiss() {
+        if (mReasonSyncOff == SyncUtil.SYNC_SETTING_GLOBAL_SYNC_OFF) {
+            SharedPreferenceUtil.incNumOfDismissesForAutoSyncOff(getContext());
+        } else if (mReasonSyncOff == SyncUtil.SYNC_SETTING_ACCOUNT_SYNC_OFF) {
+            SharedPreferenceUtil.incNumOfDismissesForAccountSyncOff(
+                    getContext(), getFilter().accountName);
+        }
+        mAlertContainer.setVisibility(View.GONE);
     }
 
     private void initSwipeRefreshLayout() {
@@ -465,7 +547,10 @@
         mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
             @Override
             public void onRefresh() {
-                syncContacts(mFilter);
+                mHandler.removeCallbacks(mCancelRefresh);
+                syncContacts(getFilter());
+                mHandler.postDelayed(mCancelRefresh, Flags.getInstance(getContext())
+                        .getInteger(Experiments.PULL_TO_REFRESH_CANCEL_REFRESH_MILLIS));
             }
         });
         mSwipeRefreshLayout.setColorSchemeResources(
@@ -503,6 +588,36 @@
         }
     }
 
+    private void setSyncOffAlert() {
+        final ContactListFilter filter = getFilter();
+        final Account account =  filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT
+                && filter.isGoogleAccountType()
+                ? new Account(filter.accountName, filter.accountType) : null;
+
+        if (account == null && !filter.isContactsFilterType()) {
+            mAlertContainer.setVisibility(View.GONE);
+        } else {
+            mReasonSyncOff = SyncUtil.calculateReasonSyncOff(getContext(), account);
+            final boolean isAlertVisible =
+                    SyncUtil.isAlertVisible(getContext(), account, mReasonSyncOff);
+            setSyncOffMsg(mReasonSyncOff);
+            mAlertContainer.setVisibility(isAlertVisible ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    private void setSyncOffMsg(int reason) {
+        final Resources resources = getResources();
+        switch (reason) {
+            case SyncUtil.SYNC_SETTING_GLOBAL_SYNC_OFF:
+                mAlertText.setText(resources.getString(R.string.auto_sync_off));
+                break;
+            case SyncUtil.SYNC_SETTING_ACCOUNT_SYNC_OFF:
+                mAlertText.setText(resources.getString(R.string.account_sync_off));
+                break;
+            default:
+        }
+    }
+
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
@@ -702,6 +817,9 @@
         updateListFilter(filter, restoreSelectedUri);
         mActivity.setTitle(AccountFilterUtil.getActionBarTitleForFilter(mActivity, filter));
 
+        // Alert user if sync is off and not dismissed before
+        setSyncOffAlert();
+
         // Determine whether the account has pullToRefresh feature
         if (Flags.getInstance(getContext()).getBoolean(Experiments.PULL_TO_REFRESH)) {
             setSwipeRefreshLayoutEnabledOrNot(filter);
diff --git a/src/com/android/contacts/list/EnableGlobalSyncDialogFragment.java b/src/com/android/contacts/list/EnableGlobalSyncDialogFragment.java
new file mode 100644
index 0000000..46df4ae
--- /dev/null
+++ b/src/com/android/contacts/list/EnableGlobalSyncDialogFragment.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2016 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.contacts.list;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import com.android.contacts.R;
+import com.android.contacts.common.list.ContactListFilter;
+
+/**
+ * Confirmation dialog for turning global auto-sync setting on.
+ */
+public class EnableGlobalSyncDialogFragment extends DialogFragment{
+
+    private static final String ARG_FILTER = "filter";
+    private ContactListFilter mFilter;
+
+    /**
+     * Callbacks for the dialog host.
+     */
+    public interface Listener {
+
+        /**
+         * Invoked after the user has confirmed that they want to turn on sync.
+         *
+         * @param filter the filter of current contacts list.
+         */
+        void onEnableAutoSync(ContactListFilter filter);
+    }
+
+    public static void show(DefaultContactBrowseListFragment fragment, ContactListFilter filter) {
+        final Bundle args = new Bundle();
+        args.putParcelable(ARG_FILTER, filter);
+
+        final EnableGlobalSyncDialogFragment dialog = new
+                EnableGlobalSyncDialogFragment();
+        dialog.setTargetFragment(fragment, 0);
+        dialog.setArguments(args);
+        dialog.show(fragment.getFragmentManager(), "globalSync");
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mFilter = getArguments().getParcelable(ARG_FILTER);
+    }
+
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        final Listener targetListener = (Listener) getTargetFragment();
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        builder.setTitle(R.string.turn_auto_sync_on_dialog_title)
+                .setMessage(R.string.turn_auto_sync_on_dialog_body)
+                .setPositiveButton(R.string.turn_auto_sync_on_dialog_confirm_btn,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int which) {
+                                    if (targetListener != null) {
+                                        targetListener.onEnableAutoSync(mFilter);
+                                    }
+                                }
+                            });
+        builder.setNegativeButton(android.R.string.cancel, null);
+        builder.setCancelable(false);
+        return builder.create();
+    }
+}
diff --git a/src/com/android/contacts/util/SharedPreferenceUtil.java b/src/com/android/contacts/util/SharedPreferenceUtil.java
index e33c7aa..3adbcc4 100644
--- a/src/com/android/contacts/util/SharedPreferenceUtil.java
+++ b/src/com/android/contacts/util/SharedPreferenceUtil.java
@@ -30,6 +30,12 @@
     private static final String PREFERENCE_KEY_HAMBURGER_PROMO_TRIGGER_ACTION_HAPPENED_BEFORE =
             "hamburgerPromoTriggerActionHappenedBefore";
 
+    public static final String PREFERENCE_KEY_GLOBAL_SYNC_OFF_DISMISSES =
+            "num-of-dismisses-auto-sync-off";
+
+    public static final String PREFERENCE_KEY_ACCOUNT_SYNC_OFF_DISMISSES
+            = "num-of-dismisses-account-sync-off";
+
     public static boolean getHamburgerPromoDisplayedBefore(Context context) {
         return getSharedPreferences(context)
                 .getBoolean(PREFERENCE_KEY_HAMBURGER_PROMO_DISPLAYED_BEFORE, false);
@@ -81,4 +87,48 @@
     private static SharedPreferences getSharedPreferences(Context context) {
         return context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
     }
+
+    public static int getNumOfDismissesForAutoSyncOff(Context context) {
+        return getSharedPreferences(context).getInt(PREFERENCE_KEY_GLOBAL_SYNC_OFF_DISMISSES, 0);
+    }
+
+    public static void resetNumOfDismissesForAutoSyncOff(Context context) {
+        final int value = getSharedPreferences(context).getInt(
+                PREFERENCE_KEY_GLOBAL_SYNC_OFF_DISMISSES, 0);
+        if (value != 0) {
+            getSharedPreferences(context).edit()
+                    .putInt(PREFERENCE_KEY_GLOBAL_SYNC_OFF_DISMISSES, 0).apply();
+        }
+    }
+
+    public static void incNumOfDismissesForAutoSyncOff(Context context) {
+        final int value = getSharedPreferences(context).getInt(
+                PREFERENCE_KEY_GLOBAL_SYNC_OFF_DISMISSES, 0);
+        getSharedPreferences(context).edit()
+                .putInt(PREFERENCE_KEY_GLOBAL_SYNC_OFF_DISMISSES, value + 1).apply();
+    }
+
+    private static String buildSharedPrefsName(String accountName) {
+        return accountName + "-" + PREFERENCE_KEY_ACCOUNT_SYNC_OFF_DISMISSES;
+    }
+
+    public static int getNumOfDismissesforAccountSyncOff(Context context, String accountName) {
+        return getSharedPreferences(context).getInt(buildSharedPrefsName(accountName), 0);
+    }
+
+    public static void resetNumOfDismissesForAccountSyncOff(Context context, String accountName) {
+        final int value = getSharedPreferences(context).getInt(
+                buildSharedPrefsName(accountName), 0);
+        if (value != 0) {
+            getSharedPreferences(context).edit()
+                    .putInt(buildSharedPrefsName(accountName), 0).apply();
+        }
+    }
+
+    public static void incNumOfDismissesForAccountSyncOff(Context context, String accountName) {
+        final int value = getSharedPreferences(context).getInt(
+                buildSharedPrefsName(accountName), 0);
+        getSharedPreferences(context).edit()
+                .putInt(buildSharedPrefsName(accountName), value + 1).apply();
+    }
 }
diff --git a/src/com/android/contacts/util/SyncUtil.java b/src/com/android/contacts/util/SyncUtil.java
index cef2223..6c17c05 100644
--- a/src/com/android/contacts/util/SyncUtil.java
+++ b/src/com/android/contacts/util/SyncUtil.java
@@ -17,6 +17,7 @@
 
 import android.accounts.Account;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.provider.ContactsContract;
 
 import com.android.contacts.common.model.account.GoogleAccountType;
@@ -29,6 +30,10 @@
 public final class SyncUtil {
     private static final String TAG = "SyncUtil";
 
+    public static final int SYNC_SETTING_SYNC_ON = 0;
+    public static final int SYNC_SETTING_GLOBAL_SYNC_OFF = 1;
+    public static final int SYNC_SETTING_ACCOUNT_SYNC_OFF = 2;
+
     private SyncUtil() {
     }
 
@@ -49,4 +54,40 @@
         }
         return ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) <= 0;
     }
-}
+
+    public static boolean isAlertVisible(Context context, Account account, int reason) {
+        if (reason == SYNC_SETTING_GLOBAL_SYNC_OFF) {
+            return (SharedPreferenceUtil.getNumOfDismissesForAutoSyncOff(context) == 0);
+        } else if (reason == SYNC_SETTING_ACCOUNT_SYNC_OFF && account != null) {
+            return (SharedPreferenceUtil.getNumOfDismissesforAccountSyncOff(
+                    context, account.name) == 0);
+        }
+        return false;
+    }
+
+    public static int calculateReasonSyncOff(Context context, Account account) {
+        // Global sync is turned off
+        if (!ContentResolver.getMasterSyncAutomatically()) {
+            if (account != null) {
+                SharedPreferenceUtil.resetNumOfDismissesForAccountSyncOff(
+                        context, account.name);
+            }
+            return SYNC_SETTING_GLOBAL_SYNC_OFF;
+        }
+
+        // Global sync is on, clear the number of times users has dismissed this
+        // alert so that next time global sync is off, alert gets displayed again.
+        SharedPreferenceUtil.resetNumOfDismissesForAutoSyncOff(context);
+        if (account != null) {
+            // Account level sync is off
+            if (!ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY)) {
+                return SYNC_SETTING_ACCOUNT_SYNC_OFF;
+            }
+            // Account sync is on, clear the number of times users has dismissed this
+            // alert so that next time sync is off, alert gets displayed again.
+            SharedPreferenceUtil.resetNumOfDismissesForAccountSyncOff(
+                    context, account.name);
+        }
+        return SYNC_SETTING_SYNC_ON;
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
index f4ec238..b701b3a 100644
--- a/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/common/test/mocks/MockAccountTypeManager.java
@@ -23,6 +23,8 @@
 import com.android.contacts.common.model.account.AccountWithDataSet;
 import com.android.contacts.common.model.account.BaseAccountType;
 import com.google.common.base.Objects;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Collections2;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
@@ -68,6 +70,11 @@
     }
 
     @Override
+    public List<AccountWithDataSet> getAccounts(Predicate<AccountWithDataSet> filter) {
+        return Lists.newArrayList(Collections2.filter(Arrays.asList(mAccounts), filter));
+    }
+
+    @Override
     public List<AccountWithDataSet> getSortedAccounts(AccountWithDataSet account,
             boolean writableOnly) {
         return Arrays.asList(mAccounts);