jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/391048 )

Change subject: Integrate with Reading List service.
......................................................................


Integrate with Reading List service.

Bug: T180189
Bug: T183610
Change-Id: Ia82c4c5288c28ae83f7a2d56d603cae14e86adb2
---
M app/build.gradle
M app/src/main/AndroidManifest.xml
M app/src/main/java/org/wikipedia/auth/WikimediaAuthenticator.java
A app/src/main/java/org/wikipedia/database/ReadingListsContentProvider.java
M app/src/main/java/org/wikipedia/dataclient/restbase/page/RbPageSummary.java
M app/src/main/java/org/wikipedia/feed/FeedFragment.java
M app/src/main/java/org/wikipedia/login/LoginActivity.java
M app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListItemView.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
M app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.java
M app/src/main/java/org/wikipedia/readinglist/database/ReadingListDbHelper.java
M app/src/main/java/org/wikipedia/readinglist/database/ReadingListPageTable.java
A app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
A app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.java
A app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncService.java
A app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java
M app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
M 
app/src/main/java/org/wikipedia/settings/DeveloperSettingsPreferenceLoader.java
M app/src/main/java/org/wikipedia/settings/Prefs.java
M app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java
M app/src/main/res/layout/fragment_reading_lists.xml
M app/src/main/res/values/preference_keys.xml
A app/src/main/res/xml/reading_list_sync_adapter.xml
25 files changed, 1,322 insertions(+), 134 deletions(-)

Approvals:
  Dbrant: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/app/build.gradle b/app/build.gradle
index 9ad7927..c720594 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -259,6 +259,8 @@
     return props
 }
 
+addSyncContentProviderAuthority 'readinglists', 'reading_lists'
+
 private void addSyncContentProviderAuthority(String path, String name) {
     android.productFlavors.all { ProductFlavor flavor ->
         String authority = "${appId(flavor)}.sync.${path}"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bdb2173..c4979b4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -248,6 +248,13 @@
             android:exported="false" />
 
         <provider
+            android:authorities="@string/reading_lists_authority"
+            android:name=".database.ReadingListsContentProvider"
+            android:exported="false"
+            android:syncable="true"
+            android:label="@string/user_option_sync_label" />
+
+        <provider
             android:name="android.support.v4.content.FileProvider"
             android:authorities="${applicationId}.fileprovider"
             android:exported="false"
@@ -314,6 +321,20 @@
         </receiver>
 
         <service
+            android:name=".readinglist.sync.ReadingListSyncService"
+            android:exported="false">
+
+            <intent-filter>
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/reading_list_sync_adapter" />
+
+        </service>
+
+        <service
             android:name=".auth.AuthenticatorService"
             android:exported="false">
 
diff --git a/app/src/main/java/org/wikipedia/auth/WikimediaAuthenticator.java 
b/app/src/main/java/org/wikipedia/auth/WikimediaAuthenticator.java
index ed29aaf..e02e0fd 100644
--- a/app/src/main/java/org/wikipedia/auth/WikimediaAuthenticator.java
+++ b/app/src/main/java/org/wikipedia/auth/WikimediaAuthenticator.java
@@ -12,12 +12,13 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import org.wikipedia.BuildConfig;
 import org.wikipedia.R;
 import org.wikipedia.analytics.LoginFunnel;
 import org.wikipedia.login.LoginActivity;
 
 public class WikimediaAuthenticator extends AbstractAccountAuthenticator {
-    private static final String[] SYNC_AUTHORITIES = {};
+    private static final String[] SYNC_AUTHORITIES = 
{BuildConfig.READING_LISTS_AUTHORITY};
 
     @NonNull private final Context context;
 
diff --git 
a/app/src/main/java/org/wikipedia/database/ReadingListsContentProvider.java 
b/app/src/main/java/org/wikipedia/database/ReadingListsContentProvider.java
new file mode 100644
index 0000000..74817a6
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/database/ReadingListsContentProvider.java
@@ -0,0 +1,4 @@
+package org.wikipedia.database;
+
+public class ReadingListsContentProvider extends AppContentProvider {
+}
diff --git 
a/app/src/main/java/org/wikipedia/dataclient/restbase/page/RbPageSummary.java 
b/app/src/main/java/org/wikipedia/dataclient/restbase/page/RbPageSummary.java
index c048db1..21667c2 100644
--- 
a/app/src/main/java/org/wikipedia/dataclient/restbase/page/RbPageSummary.java
+++ 
b/app/src/main/java/org/wikipedia/dataclient/restbase/page/RbPageSummary.java
@@ -3,6 +3,8 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import com.google.gson.annotations.SerializedName;
+
 import org.wikipedia.dataclient.page.PageSummary;
 import org.wikipedia.dataclient.restbase.RbServiceError;
 import org.wikipedia.json.annotations.Required;
@@ -22,6 +24,7 @@
     @SuppressWarnings("unused") @Nullable private String extract;
     @SuppressWarnings("unused") @Nullable private String description;
     @SuppressWarnings("unused") @Nullable private Thumbnail thumbnail;
+    @SuppressWarnings("unused") @Nullable @SerializedName("originalimage") 
private Thumbnail originalImage;
 
     @Override
     public boolean hasError() {
@@ -64,6 +67,11 @@
         return normalizedtitle == null ? title : normalizedtitle;
     }
 
+    @Nullable
+    public String getOriginalImageUrl() {
+        return originalImage == null ? null : originalImage.getUrl();
+    }
+
     /**
      * For the thumbnail URL of the page
      */
diff --git a/app/src/main/java/org/wikipedia/feed/FeedFragment.java 
b/app/src/main/java/org/wikipedia/feed/FeedFragment.java
index 1c9b4e7..9096837 100644
--- a/app/src/main/java/org/wikipedia/feed/FeedFragment.java
+++ b/app/src/main/java/org/wikipedia/feed/FeedFragment.java
@@ -41,6 +41,7 @@
 import org.wikipedia.offline.LocalCompilationsActivity;
 import org.wikipedia.offline.OfflineTutorialActivity;
 import org.wikipedia.random.RandomActivity;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.settings.SettingsActivity;
 import org.wikipedia.util.DeviceUtil;
@@ -146,6 +147,8 @@
             getCallback().updateToolbarElevation(shouldElevateToolbar());
         }
 
+        ReadingListSyncAdapter.manualSync();
+
         return view;
     }
 
diff --git a/app/src/main/java/org/wikipedia/login/LoginActivity.java 
b/app/src/main/java/org/wikipedia/login/LoginActivity.java
index cb43c1c..7959aa2 100644
--- a/app/src/main/java/org/wikipedia/login/LoginActivity.java
+++ b/app/src/main/java/org/wikipedia/login/LoginActivity.java
@@ -23,6 +23,7 @@
 import org.wikipedia.auth.AccountUtil;
 import org.wikipedia.createaccount.CreateAccountActivity;
 import org.wikipedia.page.PageTitle;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.util.log.L;
 import org.wikipedia.views.NonEmptyValidator;
@@ -241,7 +242,8 @@
                     hideSoftKeyboard(LoginActivity.this);
                     setResult(RESULT_LOGIN_SUCCESS);
 
-                    // TODO: sync/save reading list pages
+                    ReadingListSyncAdapter.manualSync();
+
                     finish();
                 } else if (result.fail()) {
                     String message = result.getMessage();
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java 
b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
index 6dce4bf..ef18d78 100644
--- a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
+++ b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
@@ -220,7 +220,7 @@
                             readingList.isDefault() ? 
getString(R.string.default_reading_list_name) : readingList.title());
                     new 
ReadingListsFunnel(title.getWikiSite()).logAddToList(readingList, 
readingLists.size(), invokeSource);
 
-                    ReadingListDbHelper.instance().addPageToList(readingList, 
title);
+                    ReadingListDbHelper.instance().addPageToList(readingList, 
title, true);
                 }
                 showViewListSnackBar(readingList, message);
                 dismiss();
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
index 021ce0f..01eca27 100644
--- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
+++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
@@ -309,7 +309,7 @@
         Snackbar snackbar = FeedbackUtil.makeSnackbar(getActivity(), message,
                 FeedbackUtil.LENGTH_DEFAULT);
         snackbar.setAction(R.string.reading_list_item_delete_undo, v -> {
-            ReadingListDbHelper.instance().addPagesToList(readingList, pages);
+            ReadingListDbHelper.instance().addPagesToList(readingList, pages, 
true);
             readingList.pages().addAll(pages);
             update();
         });
@@ -331,7 +331,7 @@
         ReadingListTitleDialog.readingListTitleDialog(getContext(), 
readingList.title(), existingTitles,
                 text -> {
                     readingList.title(text.toString());
-                    ReadingListDbHelper.instance().updateList(readingList);
+                    ReadingListDbHelper.instance().updateList(readingList, 
true);
 
                     update();
                     funnel.logModifyList(readingList, 0);
@@ -353,7 +353,8 @@
             public void onSuccess(@NonNull CharSequence text) {
 
                 readingList.description(text.toString());
-                ReadingListDbHelper.instance().updateList(readingList);
+                readingList.dirty(true);
+                ReadingListDbHelper.instance().updateList(readingList, true);
 
                 update();
                 funnel.logModifyList(readingList, 0);
@@ -429,7 +430,7 @@
         List<ReadingListPage> selectedPages = getSelectedPages();
         if (!selectedPages.isEmpty()) {
 
-            ReadingListDbHelper.instance().markPagesForDeletion(selectedPages);
+            ReadingListDbHelper.instance().markPagesForDeletion(readingList, 
selectedPages);
             readingList.pages().removeAll(selectedPages);
 
             funnel.logDeleteItem(readingList, 0);
@@ -475,7 +476,7 @@
             return;
         }
         showDeleteItemsUndoSnackbar(readingList, 
Collections.singletonList(page));
-        ReadingListDbHelper.instance().markPageForDeletion(page);
+        ReadingListDbHelper.instance().markPagesForDeletion(readingList, 
Collections.singletonList(page));
         readingList.pages().remove(page);
         funnel.logDeleteItem(readingList, 0);
         update();
@@ -710,7 +711,7 @@
 
                 page.touch();
                 CallbackTask.execute(() -> {
-                    ReadingListDbHelper.instance().updateList(readingList);
+                    ReadingListDbHelper.instance().updateList(readingList, 
false);
                     ReadingListDbHelper.instance().updatePage(page);
                 });
 
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListItemView.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListItemView.java
index 97d86bd..0103494 100644
--- a/app/src/main/java/org/wikipedia/readinglist/ReadingListItemView.java
+++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListItemView.java
@@ -131,10 +131,9 @@
         menu.getMenuInflater().inflate(R.menu.menu_reading_list_item, 
menu.getMenu());
 
         if (readingList.isDefault()) {
-            MenuItem renameItem = 
menu.getMenu().findItem(R.id.menu_reading_list_rename);
-            renameItem.setVisible(false);
-            MenuItem deleteItem = 
menu.getMenu().findItem(R.id.menu_reading_list_delete);
-            deleteItem.setVisible(false);
+            
menu.getMenu().findItem(R.id.menu_reading_list_rename).setVisible(false);
+            
menu.getMenu().findItem(R.id.menu_reading_list_edit_description).setVisible(false);
+            
menu.getMenu().findItem(R.id.menu_reading_list_delete).setVisible(false);
         }
         menu.setOnMenuItemClickListener(new OverflowMenuClickListener());
         menu.show();
@@ -155,10 +154,15 @@
         if (readingList == null) {
             return;
         }
+        defaultListEmptyView.setVisibility((readingList.isDefault() && 
readingList.pages().size() == 0) ? VISIBLE : GONE);
+        imageContainer.setVisibility(defaultListEmptyView.getVisibility() == 
VISIBLE ? GONE : VISIBLE);
         titleView.setText(readingList.isDefault()
                 ? getString(R.string.default_reading_list_name)
                 : readingList.title());
-        if (TextUtils.isEmpty(readingList.description()) && 
showDescriptionEmptyHint) {
+        if (readingList.isDefault()) {
+            
descriptionView.setText(getContext().getString(R.string.default_reading_list_description));
+            descriptionView.setTypeface(descriptionView.getTypeface(), 
Typeface.NORMAL);
+        } else if (TextUtils.isEmpty(readingList.description()) && 
showDescriptionEmptyHint) {
             
descriptionView.setText(getContext().getString(R.string.reading_list_no_description));
             descriptionView.setTypeface(descriptionView.getTypeface(), 
Typeface.ITALIC);
         } else {
@@ -177,8 +181,6 @@
         if (readingList == null) {
             return;
         }
-        defaultListEmptyView.setVisibility((readingList.isDefault() && 
readingList.pages().size() == 0) ? VISIBLE : GONE);
-        imageContainer.setVisibility(defaultListEmptyView.getVisibility() == 
VISIBLE ? GONE : VISIBLE);
         clearThumbnails();
         List<String> thumbUrls = new ArrayList<>();
         for (ReadingListPage page : readingList.pages()) {
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
index 33c073f..ec903c5 100644
--- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
+++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
@@ -7,6 +7,7 @@
 import android.support.design.widget.Snackbar;
 import android.support.v4.app.Fragment;
 import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.SwipeRefreshLayout;
 import android.support.v7.app.AppCompatActivity;
 import android.support.v7.view.ActionMode;
 import android.support.v7.widget.LinearLayoutManager;
@@ -35,6 +36,7 @@
 import org.wikipedia.readinglist.database.ReadingList;
 import org.wikipedia.readinglist.database.ReadingListDbHelper;
 import org.wikipedia.readinglist.database.ReadingListPage;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.readinglist.sync.ReadingListSyncEvent;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.settings.SettingsActivity;
@@ -51,6 +53,8 @@
 import butterknife.ButterKnife;
 import butterknife.Unbinder;
 
+import static org.wikipedia.util.ResourceUtil.getThemedAttributeId;
+
 public class ReadingListsFragment extends Fragment {
     private Unbinder unbinder;
     @BindView(R.id.reading_list_content_container) ViewGroup contentContainer;
@@ -61,6 +65,7 @@
     @BindView(R.id.empty_message) TextView emptyMessage;
     @BindView(R.id.search_empty_view) SearchEmptyView searchEmptyView;
     @BindView(R.id.reading_list_onboarding_container) ViewGroup 
onboardingContainer;
+    @BindView(R.id.reading_list_swipe_refresh) SwipeRefreshLayout 
swipeRefreshLayout;
 
     private List<ReadingList> readingLists = new ArrayList<>();
 
@@ -101,6 +106,9 @@
         WikipediaApp.getInstance().getBus().register(eventBusMethods);
 
         
contentContainer.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
+
+        
swipeRefreshLayout.setColorSchemeResources(getThemedAttributeId(getContext(), 
R.attr.colorAccent));
+        
swipeRefreshLayout.setOnRefreshListener(ReadingListSyncAdapter::manualSyncWithRefresh);
         return view;
     }
 
@@ -184,6 +192,7 @@
                 if (getActivity() == null) {
                     return;
                 }
+                swipeRefreshLayout.setRefreshing(false);
                 readingLists = lists;
                 sortLists();
                 updateEmptyState(searchQuery);
@@ -283,7 +292,9 @@
             ReadingListTitleDialog.readingListTitleDialog(getContext(), 
readingList.title(),
                     existingTitles, text -> {
                         readingList.title(text.toString());
-                        ReadingListDbHelper.instance().updateList(readingList);
+                        readingList.dirty(true);
+                        ReadingListDbHelper.instance().updateList(readingList, 
true);
+                        ReadingListSyncAdapter.manualSync();
 
                         updateLists();
                         funnel.logModifyList(readingList, readingLists.size());
@@ -302,7 +313,9 @@
                 @Override
                 public void onSuccess(@NonNull CharSequence text) {
                     readingList.description(text.toString());
-                    ReadingListDbHelper.instance().updateList(readingList);
+                    readingList.dirty(true);
+                    ReadingListDbHelper.instance().updateList(readingList, 
true);
+
                     updateLists();
                     funnel.logModifyList(readingList, readingLists.size());
                 }
@@ -354,7 +367,7 @@
             showDeleteListUndoSnackbar(readingList);
 
             ReadingListDbHelper.instance().deleteList(readingList);
-            
ReadingListDbHelper.instance().markPagesForDeletion(readingList.pages());
+            ReadingListDbHelper.instance().markPagesForDeletion(readingList, 
readingList.pages(), false);
 
             funnel.logDeleteList(readingList, readingLists.size());
             updateLists();
@@ -368,7 +381,7 @@
         snackbar.setAction(R.string.reading_list_item_delete_undo, v -> {
 
             ReadingList newList = 
ReadingListDbHelper.instance().createList(readingList.title(), 
readingList.description());
-            ReadingListDbHelper.instance().addPagesToList(newList, 
readingList.pages());
+            ReadingListDbHelper.instance().addPagesToList(newList, 
readingList.pages(), true);
             updateLists();
         });
         snackbar.show();
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.java 
b/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.java
index 48eab7a..cecac24 100644
--- 
a/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.java
+++ 
b/app/src/main/java/org/wikipedia/readinglist/RemoveFromReadingListsDialog.java
@@ -10,6 +10,7 @@
 import org.wikipedia.readinglist.database.ReadingListDbHelper;
 import org.wikipedia.readinglist.database.ReadingListPage;
 
+import java.util.Collections;
 import java.util.List;
 
 public class RemoveFromReadingListsDialog {
@@ -28,7 +29,8 @@
             return;
         }
         if (listsContainingPage.size() == 1 && 
!listsContainingPage.get(0).pages().isEmpty()) {
-            
ReadingListDbHelper.instance().markPageForDeletion(listsContainingPage.get(0).pages().get(0));
+            
ReadingListDbHelper.instance().markPagesForDeletion(listsContainingPage.get(0),
+                    
Collections.singletonList(listsContainingPage.get(0).pages().get(0)));
             if (callback != null) {
                 callback.onDeleted(listsContainingPage.get(0).pages().get(0));
             }
@@ -52,7 +54,8 @@
                     for (int i = 0; i < listNames.length; i++) {
                         if (selected[i]) {
                             atLeastOneSelected = true;
-                            
ReadingListDbHelper.instance().markPageForDeletion(listsContainingPage.get(i).pages().get(0));
+                            
ReadingListDbHelper.instance().markPagesForDeletion(listsContainingPage.get(i),
+                                    
Collections.singletonList(listsContainingPage.get(i).pages().get(0)));
                         }
                     }
                     if (callback != null && atLeastOneSelected) {
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListDbHelper.java 
b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListDbHelper.java
index d113e3e..60ef621 100644
--- 
a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListDbHelper.java
+++ 
b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListDbHelper.java
@@ -10,10 +10,12 @@
 import org.wikipedia.database.contract.ReadingListContract;
 import org.wikipedia.database.contract.ReadingListPageContract;
 import org.wikipedia.page.PageTitle;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.savedpages.SavedPageSyncService;
 import org.wikipedia.util.log.L;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Random;
 
@@ -54,6 +56,20 @@
         return lists;
     }
 
+    public List<ReadingList> getAllListsWithUnsyncedPages() {
+        List<ReadingList> lists = getAllListsWithoutContents();
+        List<ReadingListPage> pages = getAllPagesToBeSynced();
+        for (ReadingListPage page : pages) {
+            for (ReadingList list : lists) {
+                if (page.listId() == list.id()) {
+                    list.pages().add(page);
+                    break;
+                }
+            }
+        }
+        return lists;
+    }
+
     @NonNull
     public ReadingList createList(@NonNull String title, @Nullable String 
description) {
         SQLiteDatabase db = getWritableDatabase();
@@ -75,28 +91,38 @@
         }
     }
 
-    public void updateList(@NonNull ReadingList list) {
+    public void updateList(@NonNull ReadingList list, boolean queueForSync) {
         SQLiteDatabase db = getWritableDatabase();
-        updateList(db, list);
+        updateLists(db, Collections.singletonList(list), queueForSync);
     }
 
-    public void updateList(@NonNull SQLiteDatabase db, @NonNull ReadingList 
list) {
+    public void updateList(@NonNull SQLiteDatabase db, @NonNull ReadingList 
list, boolean queueForSync) {
+        updateLists(db, Collections.singletonList(list), queueForSync);
+    }
+
+    private void updateLists(SQLiteDatabase db, @NonNull List<ReadingList> 
lists, boolean queueForSync) {
         db.beginTransaction();
         try {
-            // implicitly update the last-access time of the list
-            list.touch();
-            int result = db.update(ReadingListContract.TABLE, 
ReadingList.DATABASE_TABLE.toContentValues(list),
-                    ReadingListContract.Col.ID.getName() + " = ?", new 
String[]{Long.toString(list.id())});
-            if (result != 1) {
-                L.w("Failed to update db entry for list " + list.title());
+            for (ReadingList list : lists) {
+                if (queueForSync) {
+                    list.dirty(true);
+                }
+                updateListInDb(db, list);
             }
             db.setTransactionSuccessful();
         } finally {
             db.endTransaction();
         }
+        if (queueForSync) {
+            ReadingListSyncAdapter.manualSync();
+        }
     }
 
     public void deleteList(@NonNull ReadingList list) {
+        deleteList(list, true);
+    }
+
+    public void deleteList(@NonNull ReadingList list, boolean queueForSync) {
         SQLiteDatabase db = getWritableDatabase();
         db.beginTransaction();
         try {
@@ -106,12 +132,15 @@
                 L.w("Failed to delete db entry for list " + list.title());
             }
             db.setTransactionSuccessful();
+            if (queueForSync) {
+                ReadingListSyncAdapter.manualSyncWithDeleteList(list);
+            }
         } finally {
             db.endTransaction();
         }
     }
 
-    public void addPageToList(@NonNull ReadingList list, @NonNull PageTitle 
title) {
+    public void addPageToList(@NonNull ReadingList list, @NonNull PageTitle 
title, boolean queueForSync) {
         SQLiteDatabase db = getWritableDatabase();
         db.beginTransaction();
         try {
@@ -121,14 +150,20 @@
             db.endTransaction();
         }
         SavedPageSyncService.enqueue();
+        if (queueForSync) {
+            ReadingListSyncAdapter.manualSync();
+        }
     }
 
-    public void addPagesToList(@NonNull ReadingList list, @NonNull 
List<ReadingListPage> pages) {
+    public void addPagesToList(@NonNull ReadingList list, @NonNull 
List<ReadingListPage> pages, boolean queueForSync) {
         SQLiteDatabase db = getWritableDatabase();
         addPagesToList(db, list, pages);
+        if (queueForSync) {
+            ReadingListSyncAdapter.manualSync();
+        }
     }
 
-    public void addPagesToList(@NonNull SQLiteDatabase db, @NonNull 
ReadingList list, @NonNull List<ReadingListPage> pages) {
+    void addPagesToList(@NonNull SQLiteDatabase db, @NonNull ReadingList list, 
@NonNull List<ReadingListPage> pages) {
         db.beginTransaction();
         try {
             for (ReadingListPage page : pages) {
@@ -147,7 +182,7 @@
         int numAdded = 0;
         try {
             for (PageTitle title : titles) {
-                if (pageExistsInList(db, list, title)) {
+                if (getPageByTitle(db, list, title) != null) {
                     continue;
                 }
                 addPageToList(db, list, title);
@@ -157,24 +192,23 @@
         } finally {
             db.endTransaction();
         }
-        SavedPageSyncService.enqueue();
+        if (numAdded > 0) {
+            SavedPageSyncService.enqueue();
+            ReadingListSyncAdapter.manualSync();
+        }
         return numAdded;
     }
 
-    public void markPageForDeletion(@NonNull ReadingListPage page) {
-        SQLiteDatabase db = getWritableDatabase();
-        db.beginTransaction();
-        try {
-            page.status(ReadingListPage.STATUS_QUEUE_FOR_DELETE);
-            updatePageInDb(db, page);
-            db.setTransactionSuccessful();
-        } finally {
-            db.endTransaction();
-        }
-        SavedPageSyncService.enqueue();
+    private void addPageToList(SQLiteDatabase db, @NonNull ReadingList list, 
@NonNull PageTitle title) {
+        ReadingListPage protoPage = new ReadingListPage(title);
+        insertPageInDb(db, list, protoPage);
     }
 
-    public void markPagesForDeletion(@NonNull List<ReadingListPage> pages) {
+    public void markPagesForDeletion(@NonNull ReadingList list, @NonNull 
List<ReadingListPage> pages) {
+        markPagesForDeletion(list, pages, true);
+    }
+
+    public void markPagesForDeletion(@NonNull ReadingList list, @NonNull 
List<ReadingListPage> pages, boolean queueForSync) {
         SQLiteDatabase db = getWritableDatabase();
         db.beginTransaction();
         try {
@@ -183,6 +217,9 @@
                 updatePageInDb(db, page);
             }
             db.setTransactionSuccessful();
+            if (queueForSync) {
+                ReadingListSyncAdapter.manualSyncWithDeletePages(list, pages);
+            }
         } finally {
             db.endTransaction();
         }
@@ -223,6 +260,24 @@
         SavedPageSyncService.enqueue();
     }
 
+    public void markEverythingUnsynced() {
+        SQLiteDatabase db = getWritableDatabase();
+        db.beginTransaction();
+        try {
+            ContentValues contentValues = new ContentValues();
+            contentValues.put(ReadingListContract.Col.REMOTEID.getName(), -1);
+            int result = db.update(ReadingListContract.TABLE, contentValues, 
null, null);
+            L.d("Updated " + result + " lists in db.");
+            contentValues = new ContentValues();
+            contentValues.put(ReadingListPageContract.Col.REMOTEID.getName(), 
-1);
+            result = db.update(ReadingListPageContract.TABLE, contentValues, 
null, null);
+            L.d("Updated " + result + " pages in db.");
+            db.setTransactionSuccessful();
+        } finally {
+            db.endTransaction();
+        }
+    }
+
     public void updatePage(@NonNull ReadingListPage page) {
         SQLiteDatabase db = getWritableDatabase();
         db.beginTransaction();
@@ -254,6 +309,16 @@
                 ReadingListPageContract.Col.ID.getName() + " = ?", new 
String[]{Long.toString(page.id())});
         if (result != 1) {
             L.w("Failed to delete db entry for page " + page.title());
+        }
+    }
+
+    private void updateListInDb(@NonNull SQLiteDatabase db, @NonNull 
ReadingList list) {
+        // implicitly update the last-access time of the list
+        list.touch();
+        int result = db.update(ReadingListContract.TABLE, 
ReadingList.DATABASE_TABLE.toContentValues(list),
+                ReadingListContract.Col.ID.getName() + " = ?", new 
String[]{Long.toString(list.id())});
+        if (result != 1) {
+            L.w("Failed to update db entry for list " + list.title());
         }
     }
 
@@ -294,7 +359,13 @@
 
     public boolean pageExistsInList(@NonNull ReadingList list, @NonNull 
PageTitle title) {
         SQLiteDatabase db = getReadableDatabase();
-        return pageExistsInList(db, list, title);
+        return getPageByTitle(db, list, title) != null;
+    }
+
+    @Nullable
+    public ReadingListPage getPageByTitle(@NonNull ReadingList list, @NonNull 
PageTitle title) {
+        SQLiteDatabase db = getReadableDatabase();
+        return getPageByTitle(db, list, title);
     }
 
     @NonNull
@@ -393,6 +464,21 @@
         return pages;
     }
 
+    @NonNull
+    private List<ReadingListPage> getAllPagesToBeSynced() {
+        List<ReadingListPage> pages = new ArrayList<>();
+        SQLiteDatabase db = getReadableDatabase();
+        try (Cursor cursor = db.query(ReadingListPageContract.TABLE, null,
+                ReadingListPageContract.Col.REMOTEID.getName() + " < ?",
+                new String[]{Integer.toString(1)},
+                null, null, null)) {
+            while (cursor.moveToNext()) {
+                pages.add(ReadingListPage.DATABASE_TABLE.fromCursor(cursor));
+            }
+        }
+        return pages;
+    }
+
     public void resetUnsavedPageStatus() {
         SQLiteDatabase db = getWritableDatabase();
         db.beginTransaction();
@@ -444,8 +530,7 @@
 
     private void populateListPages(SQLiteDatabase db, @NonNull ReadingList 
list) {
         try (Cursor cursor = db.query(ReadingListPageContract.TABLE, null,
-                ReadingListPageContract.Col.LISTID.getName() + " = ? AND "
-                + ReadingListPageContract.Col.STATUS.getName() + " != ?",
+                (ReadingListPageContract.Col.LISTID.getName() + " = ? AND " + 
ReadingListPageContract.Col.STATUS.getName() + " != ?"),
                 new String[]{Long.toString(list.id()), 
Integer.toString(ReadingListPage.STATUS_QUEUE_FOR_DELETE)},
                 null, null, null)) {
             while (cursor.moveToNext()) {
@@ -454,7 +539,8 @@
         }
     }
 
-    private boolean pageExistsInList(SQLiteDatabase db, @NonNull ReadingList 
list, @NonNull PageTitle title) {
+    @Nullable
+    private ReadingListPage getPageByTitle(SQLiteDatabase db, @NonNull 
ReadingList list, @NonNull PageTitle title) {
         try (Cursor cursor = db.query(ReadingListPageContract.TABLE, null,
                 ReadingListPageContract.Col.SITE.getName() + " = ? AND "
                         + ReadingListPageContract.Col.LANG.getName() + " = ? 
AND "
@@ -467,18 +553,12 @@
                         Long.toString(list.id()),
                         
Integer.toString(ReadingListPage.STATUS_QUEUE_FOR_DELETE)},
                 null, null, null)) {
-            if (cursor.getCount() > 0) {
-                return true;
+            if (cursor.moveToNext()) {
+                return ReadingListPage.DATABASE_TABLE.fromCursor(cursor);
             }
         }
-        return false;
+        return null;
     }
-
-    private void addPageToList(SQLiteDatabase db, @NonNull ReadingList list, 
@NonNull PageTitle title) {
-        ReadingListPage protoPage = new ReadingListPage(title);
-        insertPageInDb(db, list, protoPage);
-    }
-
 
     private SQLiteDatabase getReadableDatabase() {
         return WikipediaApp.getInstance().getDatabase().getReadableDatabase();
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListPageTable.java
 
b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListPageTable.java
index 02a5bc7..262a842 100644
--- 
a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListPageTable.java
+++ 
b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListPageTable.java
@@ -133,7 +133,7 @@
         for (ReadingList list : lists) {
             if 
(list.title().equalsIgnoreCase(WikipediaApp.getInstance().getString(R.string.default_reading_list_name)))
 {
                 
list.title(String.format(WikipediaApp.getInstance().getString(R.string.reading_list_saved_list_rename),
 list.title()));
-                ReadingListDbHelper.instance().updateList(db, list);
+                ReadingListDbHelper.instance().updateList(db, list, false);
             }
         }
     }
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
new file mode 100644
index 0000000..2dd6016
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
@@ -0,0 +1,262 @@
+package org.wikipedia.readinglist.sync;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.dataclient.okhttp.HttpStatusException;
+import org.wikipedia.dataclient.retrofit.RbCachedService;
+import org.wikipedia.dataclient.retrofit.WikiCachedService;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.Response;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
+import retrofit2.http.GET;
+import retrofit2.http.POST;
+import retrofit2.http.PUT;
+import retrofit2.http.Path;
+import retrofit2.http.Query;
+
+import static 
org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingList;
+import static 
org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingListEntry;
+
+public class ReadingListClient {
+    @NonNull private final WikiCachedService<Service> cachedService = new 
RbCachedService<>(Service.class);
+    @NonNull private final WikiSite wiki;
+    @Nullable private String lastDateHeader;
+
+    // Artificial upper limit on the number of continuation cycles we can do, 
to prevent
+    // getting stuck in an infinite loop.
+    private static final int MAX_CONTINUE_CYCLES = 256;
+
+    public ReadingListClient(@NonNull WikiSite wiki) {
+        this.wiki = wiki;
+    }
+
+    @Nullable public String getLastDateHeader() {
+        return lastDateHeader;
+    }
+
+    public void setup(@NonNull String csrfToken) throws Throwable {
+        try {
+            cachedService.service(wiki).setup(csrfToken).execute();
+        } catch (Throwable t) {
+            if (isErrorType(t, "already-set-up")) {
+                return;
+            }
+            throw t;
+        }
+    }
+
+    public void tearDown(@NonNull String csrfToken) throws Throwable {
+        try {
+            cachedService.service(wiki).tearDown(csrfToken).execute();
+        } catch (Throwable t) {
+            if (isErrorType(t, "not-set-up")) {
+                return;
+            }
+            throw t;
+        }
+    }
+
+    @NonNull
+    public List<RemoteReadingList> getAllLists() throws Throwable {
+        List<RemoteReadingList> totalLists = new ArrayList<>();
+        int totalCycles = 0;
+        String continueStr = null;
+        do {
+            Response<SyncedReadingLists> response = 
cachedService.service(wiki).getLists(continueStr).execute();
+            SyncedReadingLists lists = response.body();
+            if (lists == null || lists.getLists() == null) {
+                throw new IOException("Incorrect response format.");
+            }
+            totalLists.addAll(lists.getLists());
+            continueStr = TextUtils.isEmpty(lists.getContinueStr()) ? null : 
lists.getContinueStr();
+            saveLastDateHeader(response);
+        } while (!TextUtils.isEmpty(continueStr) && (totalCycles++ < 
MAX_CONTINUE_CYCLES));
+        return totalLists;
+    }
+
+    @NonNull
+    public SyncedReadingLists getChangesSince(@NonNull String date) throws 
Throwable {
+        List<RemoteReadingList> totalLists = new ArrayList<>();
+        List<RemoteReadingListEntry> totalEntries = new ArrayList<>();
+        int totalCycles = 0;
+        String continueStr = null;
+        do {
+            Response<SyncedReadingLists> response = 
cachedService.service(wiki).getChangesSince(date, continueStr).execute();
+            SyncedReadingLists body = response.body();
+            if (body == null) {
+                throw new IOException("Incorrect response format.");
+            }
+            if (body.getLists() != null) {
+                totalLists.addAll(body.getLists());
+            }
+            if (body.getEntries() != null) {
+                totalEntries.addAll(body.getEntries());
+            }
+            continueStr = TextUtils.isEmpty(body.getContinueStr()) ? null : 
body.getContinueStr();
+            saveLastDateHeader(response);
+        } while (!TextUtils.isEmpty(continueStr) && (totalCycles++ < 
MAX_CONTINUE_CYCLES));
+        return new SyncedReadingLists(totalLists, totalEntries);
+    }
+
+    @NonNull
+    public List<RemoteReadingList> getListsContaining(@NonNull 
RemoteReadingListEntry entry) throws Throwable {
+        List<RemoteReadingList> totalLists = new ArrayList<>();
+        int totalCycles = 0;
+        String continueStr = null;
+        do {
+            Response<SyncedReadingLists> response = cachedService.service(wiki)
+                    .getListsContaining(entry.project(), entry.title(), 
continueStr).execute();
+            SyncedReadingLists lists = response.body();
+            if (lists == null || lists.getLists() == null) {
+                throw new IOException("Incorrect response format.");
+            }
+            totalLists.addAll(lists.getLists());
+            continueStr = TextUtils.isEmpty(lists.getContinueStr()) ? null : 
lists.getContinueStr();
+            saveLastDateHeader(response);
+        } while (!TextUtils.isEmpty(continueStr) && (totalCycles++ < 
MAX_CONTINUE_CYCLES));
+        return totalLists;
+    }
+
+    @NonNull
+    public List<RemoteReadingListEntry> getListEntries(long listId) throws 
Throwable {
+        List<RemoteReadingListEntry> totalEntries = new ArrayList<>();
+        int totalCycles = 0;
+        String continueStr = null;
+        do {
+            Response<SyncedReadingLists> response
+                    = cachedService.service(wiki).getListEntries(listId, 
continueStr).execute();
+            SyncedReadingLists body = response.body();
+            if (body == null || body.getEntries() == null) {
+                throw new IOException("Incorrect response format.");
+            }
+            totalEntries.addAll(body.getEntries());
+            continueStr = TextUtils.isEmpty(body.getContinueStr()) ? null : 
body.getContinueStr();
+            saveLastDateHeader(response);
+        } while (!TextUtils.isEmpty(continueStr) && (totalCycles++ < 
MAX_CONTINUE_CYCLES));
+        return totalEntries;
+    }
+
+    public long createList(@NonNull String csrfToken, @NonNull 
RemoteReadingList list) throws Throwable {
+        Response<SyncedReadingLists.RemoteIdResponse> response
+                = cachedService.service(wiki).createList(csrfToken, 
list).execute();
+        SyncedReadingLists.RemoteIdResponse idResponse = response.body();
+        if (idResponse == null) {
+            throw new IOException("Incorrect response format.");
+        }
+        saveLastDateHeader(response);
+        return idResponse.id();
+    }
+
+    public void updateList(@NonNull String csrfToken, long listId, @NonNull 
RemoteReadingList list) throws Throwable {
+        Response response = cachedService.service(wiki).updateList(listId, 
csrfToken, list).execute();
+        saveLastDateHeader(response);
+    }
+
+    public void deleteList(@NonNull String csrfToken, long listId) throws 
Throwable {
+        Response response = cachedService.service(wiki).deleteList(listId, 
csrfToken).execute();
+        saveLastDateHeader(response);
+    }
+
+    public long addPageToList(@NonNull String csrfToken, long listId, @NonNull 
RemoteReadingListEntry entry) throws Throwable {
+        Response<SyncedReadingLists.RemoteIdResponse> response
+                = cachedService.service(wiki).addEntryToList(listId, 
csrfToken, entry).execute();
+        SyncedReadingLists.RemoteIdResponse idResponse = response.body();
+        if (idResponse == null) {
+            throw new IOException("Incorrect response format.");
+        }
+        saveLastDateHeader(response);
+        return idResponse.id();
+    }
+
+    public void deletePageFromList(@NonNull String csrfToken, long listId, 
long entryId) throws Throwable {
+        Response response = 
cachedService.service(wiki).deleteEntryFromList(listId, entryId, 
csrfToken).execute();
+        saveLastDateHeader(response);
+    }
+
+    public boolean isErrorType(Throwable t, @NonNull String errorType) {
+        return (t instanceof HttpStatusException
+                && ((HttpStatusException) t).serviceError() != null
+                && ((HttpStatusException) 
t).serviceError().getTitle().contains(errorType));
+    }
+
+    public boolean isServiceError(Throwable t) {
+        final int code = 400;
+        return (t instanceof HttpStatusException && ((HttpStatusException) 
t).code() == code);
+    }
+
+    public boolean isUnavailableError(Throwable t) {
+        final int code = 405;
+        return (t instanceof HttpStatusException && ((HttpStatusException) 
t).code() == code);
+    }
+
+    private void saveLastDateHeader(@NonNull Response response) {
+        lastDateHeader = response.headers().get("date");
+    }
+
+    // Documentation: https://en.wikipedia.org/api/rest_v1/#/Reading_lists
+    private interface Service {
+
+        @POST("data/lists/setup")
+        @NonNull
+        Call<Void> setup(@Query("csrf_token") String token);
+
+        @POST("data/lists/teardown")
+        @NonNull
+        Call<Void> tearDown(@Query("csrf_token") String token);
+
+        @GET("data/lists/")
+        @NonNull
+        Call<SyncedReadingLists> getLists(@Query("next") String next);
+
+        @POST("data/lists/")
+        @NonNull
+        Call<SyncedReadingLists.RemoteIdResponse> 
createList(@Query("csrf_token") String token,
+                                                             @Body 
RemoteReadingList list);
+
+        @PUT("data/lists/{id}")
+        @NonNull
+        Call<Void> updateList(@Path("id") long listId, @Query("csrf_token") 
String token,
+                              @Body RemoteReadingList list);
+
+        @DELETE("data/lists/{id}")
+        @NonNull
+        Call<Void> deleteList(@Path("id") long listId, @Query("csrf_token") 
String token);
+
+        @GET("data/lists/changes/since/{date}")
+        @NonNull
+        Call<SyncedReadingLists> getChangesSince(@Path("date") String 
iso8601Date,
+                                                 @Query("next") String next);
+
+        @GET("data/lists/pages/{project}/{title}")
+        @NonNull
+        Call<SyncedReadingLists> getListsContaining(@Path("project") String 
project,
+                                                    @Path("title") String 
title,
+                                                    @Query("next") String 
next);
+
+        @GET("data/lists/{id}/entries/")
+        @NonNull
+        Call<SyncedReadingLists> getListEntries(@Path("id") long listId,
+                                                @Query("next") String next);
+
+        @POST("data/lists/{id}/entries/")
+        @NonNull
+        Call<SyncedReadingLists.RemoteIdResponse> addEntryToList(@Path("id") 
long listId,
+                                                                 
@Query("csrf_token") String token,
+                                                                 @Body 
RemoteReadingListEntry entry);
+
+        @DELETE("data/lists/{id}/entries/{entry_id}")
+        @NonNull
+        Call<Void> deleteEntryFromList(@Path("id") long listId, 
@Path("entry_id") long entryId,
+                                       @Query("csrf_token") String token);
+
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.java
new file mode 100644
index 0000000..eaccd29
--- /dev/null
+++ 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.java
@@ -0,0 +1,539 @@
+package org.wikipedia.readinglist.sync;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+import org.wikipedia.BuildConfig;
+import org.wikipedia.WikipediaApp;
+import org.wikipedia.auth.AccountUtil;
+import org.wikipedia.csrf.CsrfTokenClient;
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.page.PageTitle;
+import org.wikipedia.readinglist.database.ReadingList;
+import org.wikipedia.readinglist.database.ReadingListDbHelper;
+import org.wikipedia.readinglist.database.ReadingListPage;
+import org.wikipedia.savedpages.SavedPageSyncService;
+import org.wikipedia.settings.Prefs;
+import org.wikipedia.util.DateUtil;
+import org.wikipedia.util.ReleaseUtil;
+import org.wikipedia.util.log.L;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static 
org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingList;
+import static 
org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingListEntry;
+
+public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter {
+    private static final String SYNC_EXTRAS_FORCE_FULL_SYNC = "forceFullSync";
+    private static final String SYNC_EXTRAS_REFRESHING = "refreshing";
+    private static final String SYNC_EXTRAS_RETRYING = "retrying";
+
+    public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
+        super(context, autoInitialize);
+    }
+
+    public ReadingListSyncAdapter(Context context, boolean autoInitialize, 
boolean allowParallelSyncs) {
+        super(context, autoInitialize, allowParallelSyncs);
+    }
+
+    public static void manualSyncWithDeleteList(@NonNull ReadingList list) {
+        if (list.remoteId() <= 0) {
+            return;
+        }
+        
Prefs.setReadingListsDeletedIds(Collections.singleton(list.remoteId()));
+        manualSync();
+    }
+
+    public static void manualSyncWithDeletePages(@NonNull ReadingList list, 
@NonNull List<ReadingListPage> pages) {
+        if (list.remoteId() <= 0) {
+            return;
+        }
+        Set<String> ids = new HashSet<>();
+        for (ReadingListPage page : pages) {
+            if (page.remoteId() > 0) {
+                ids.add(Long.toString(list.remoteId()) + ":" + 
Long.toString(page.remoteId()));
+            }
+        }
+        if (!ids.isEmpty()) {
+            Prefs.setReadingListPagesDeletedIds(ids);
+            manualSync();
+        }
+    }
+
+    public static void manualSyncWithForce() {
+        Bundle extras = new Bundle();
+        extras.putBoolean(SYNC_EXTRAS_FORCE_FULL_SYNC, true);
+        manualSync(extras);
+    }
+
+    public static void manualSyncWithRefresh() {
+        Bundle extras = new Bundle();
+        extras.putBoolean(SYNC_EXTRAS_REFRESHING, true);
+        manualSync(extras);
+    }
+
+    public static void manualSync() {
+        manualSync(new Bundle());
+    }
+
+    private static void manualSync(@NonNull Bundle extras) {
+        if (AccountUtil.account() == null) {
+            if (extras.containsKey(SYNC_EXTRAS_REFRESHING)) {
+                SavedPageSyncService.sendSyncEvent();
+            }
+            return;
+        }
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+        ContentResolver.requestSync(AccountUtil.account(), 
BuildConfig.READING_LISTS_AUTHORITY, extras);
+    }
+
+    @SuppressWarnings("methodlength")
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority,
+                              ContentProviderClient provider, SyncResult 
syncResult) {
+        if (!ReleaseUtil.isPreBetaRelease()  // TODO: remove when ready for 
beta/production
+                || !AccountUtil.isLoggedIn()
+                || !(Prefs.isReadingListSyncEnabled()
+                || Prefs.isReadingListsRemoteDeletePending())) {
+            L.d("Skipping sync of reading lists.");
+            if (extras.containsKey(SYNC_EXTRAS_REFRESHING)) {
+                SavedPageSyncService.sendSyncEvent();
+            }
+            return;
+        }
+
+        // TODO: handle this:
+        // If the current user name is not the same as the stored reading list 
user name, then
+        // we should delete the current list collection and resync it from the 
server.
+        //Prefs.setReadingListsCurrentUser(AccountUtil.getUserName());
+
+        L.d("Begin sync of reading lists...");
+
+        List<String> csrfToken = new ArrayList<>();
+        Set<Long> listIdsDeleted = Prefs.getReadingListsDeletedIds();
+        Set<String> pageIdsDeleted = Prefs.getReadingListPagesDeletedIds();
+
+        List<ReadingList> allLocalLists = null;
+
+        WikiSite wiki = WikipediaApp.getInstance().getWikiSite();
+        ReadingListClient client = new ReadingListClient(wiki);
+
+        String lastSyncTime = Prefs.getReadingListsLastSyncTime();
+        boolean shouldSendSyncEvent = 
extras.containsKey(SYNC_EXTRAS_REFRESHING);
+        boolean shouldRetry = false;
+        boolean shouldRetryWithForce = false;
+
+        try {
+            boolean syncEverything = false;
+
+            if (extras.containsKey(SYNC_EXTRAS_FORCE_FULL_SYNC)
+                    || Prefs.isReadingListsRemoteDeletePending()
+                    || Prefs.isReadingListsRemoteSetupPending()) {
+                // reset the remote ID on all lists, since they will need to 
be recreated next time.
+                L.d("Resetting all lists to un-synced.");
+                syncEverything = true;
+                ReadingListDbHelper.instance().markEverythingUnsynced();
+                allLocalLists = ReadingListDbHelper.instance().getAllLists();
+            }
+
+            if (Prefs.isReadingListsRemoteDeletePending()) {
+                // Are we scheduled for a teardown? If so, delete everything 
and bail.
+                L.d("Tearing down remote lists...");
+                client.tearDown(getCsrfToken(wiki, csrfToken));
+                Prefs.setReadingListsRemoteDeletePending(false);
+                return;
+            } else if (Prefs.isReadingListsRemoteSetupPending()) {
+                // ...Or are we scheduled for setup?
+                client.setup(getCsrfToken(wiki, csrfToken));
+                Prefs.setReadingListsRemoteSetupPending(false);
+            }
+
+            //-----------------------------------------------
+            // PHASE 1: Sync from remote to local.
+            //-----------------------------------------------
+
+            List<RemoteReadingList> remoteListsModified = 
Collections.emptyList();
+            List<RemoteReadingListEntry> remoteEntriesModified = 
Collections.emptyList();
+
+            if (TextUtils.isEmpty(lastSyncTime)) {
+                syncEverything = true;
+            }
+
+            if (syncEverything) {
+                if (allLocalLists == null) {
+                    allLocalLists = 
ReadingListDbHelper.instance().getAllLists();
+                }
+            } else {
+                if (allLocalLists == null) {
+                    allLocalLists = 
ReadingListDbHelper.instance().getAllListsWithUnsyncedPages();
+                }
+                L.d("Fetching changes from server, since " + lastSyncTime);
+                SyncedReadingLists allChanges = 
client.getChangesSince(lastSyncTime);
+                if (allChanges.getLists() != null) {
+                    remoteListsModified = allChanges.getLists();
+                }
+                if (allChanges.getEntries() != null) {
+                    remoteEntriesModified = allChanges.getEntries();
+                }
+            }
+
+            // Perform a quick check for whether we'll need to sync all lists
+            for (RemoteReadingListEntry remoteEntry : remoteEntriesModified) {
+                // find the list to which this entry belongs...
+                ReadingList eigenLocalList = null;
+                RemoteReadingList eigenRemoteList = null;
+                for (ReadingList localList : allLocalLists) {
+                    if (localList.remoteId() == remoteEntry.listId()) {
+                        eigenLocalList = localList;
+                        break;
+                    }
+                }
+                for (RemoteReadingList remoteList : remoteListsModified) {
+                    if (remoteList.id() == remoteEntry.listId()) {
+                        eigenRemoteList = remoteList;
+                        break;
+                    }
+                }
+                if (eigenLocalList == null && eigenRemoteList == null) {
+                    L.w("Remote entry belongs to an unknown local list. 
Falling back to full sync.");
+                    syncEverything = true;
+                    break;
+                }
+            }
+
+            if (syncEverything) {
+                allLocalLists = ReadingListDbHelper.instance().getAllLists();
+                L.d("Fetching all lists from server...");
+                remoteListsModified = client.getAllLists();
+            }
+
+            // First, update our list hierarchy to match the remote hierarchy.
+            for (RemoteReadingList remoteList : remoteListsModified) {
+                // Find the remote list in our local lists...
+                ReadingList localList = null;
+                boolean upsertNeeded = false;
+
+                for (ReadingList list : allLocalLists) {
+                    if (list.isDefault() && remoteList.isDefault()) {
+                        localList = list;
+                        if (list.remoteId() != remoteList.id()) {
+                            list.remoteId(remoteList.id());
+                            upsertNeeded = true;
+                        }
+                        break;
+                    }
+                    if (list.remoteId() == remoteList.id()) {
+                        localList = list;
+                        break;
+                    } else if (list.title().equals(remoteList.name())) {
+                        list.remoteId(remoteList.id());
+                        upsertNeeded = true;
+                        localList = list;
+                    }
+                }
+
+                if (remoteList.isDeleted()) {
+                    if (localList != null && !localList.isDefault()) {
+                        L.d("Deleting local list " + localList.title());
+                        ReadingListDbHelper.instance().deleteList(localList, 
false);
+                        
ReadingListDbHelper.instance().markPagesForDeletion(localList, 
localList.pages(), false);
+                        allLocalLists.remove(localList);
+                        shouldSendSyncEvent = true;
+                    }
+                    continue;
+                }
+
+                if (localList == null) {
+                    // A new list needs to be created locally.
+                    L.d("Creating local list " + remoteList.name());
+                    localList = 
ReadingListDbHelper.instance().createList(remoteList.name(), 
remoteList.description());
+                    localList.remoteId(remoteList.id());
+                    allLocalLists.add(localList);
+                    upsertNeeded = true;
+                } else {
+                    if (!localList.isDefault() && 
!localList.title().equals(remoteList.name())) {
+                        localList.title(remoteList.name());
+                        upsertNeeded = true;
+                    }
+                    if (!localList.isDefault() && 
!TextUtils.equals(localList.description(), remoteList.description())) {
+                        localList.description(remoteList.description());
+                        upsertNeeded = true;
+                    }
+                }
+                if (upsertNeeded) {
+                    L.d("Updating info for local list " + localList.title());
+                    localList.dirty(false);
+                    ReadingListDbHelper.instance().updateList(localList, 
false);
+                    shouldSendSyncEvent = true;
+                }
+
+                if (syncEverything) {
+                    L.d("Fetching all pages in remote list " + 
remoteList.name());
+                    List<RemoteReadingListEntry> remoteEntries = 
client.getListEntries(remoteList.id());
+                    for (RemoteReadingListEntry remoteEntry : remoteEntries) {
+                        // TODO: optimization opportunity -- create/update 
local pages in bulk.
+                        createOrUpdatePage(localList, remoteEntry);
+                    }
+                    shouldSendSyncEvent = true;
+                }
+            }
+
+            if (!syncEverything) {
+                for (RemoteReadingListEntry remoteEntry : 
remoteEntriesModified) {
+                    // find the list to which this entry belongs...
+                    ReadingList eigenList = null;
+                    for (ReadingList localList : allLocalLists) {
+                        if (localList.remoteId() == remoteEntry.listId()) {
+                            eigenList = localList;
+                            break;
+                        }
+                    }
+                    if (eigenList == null) {
+                        L.w("Remote entry belongs to an unknown local list.");
+                        continue;
+                    }
+                    shouldSendSyncEvent = true;
+                    if (remoteEntry.isDeleted()) {
+                        deletePageByTitle(eigenList, 
pageTitleFromRemoteEntry(remoteEntry));
+                    } else {
+                        createOrUpdatePage(eigenList, remoteEntry);
+                    }
+                }
+            }
+
+            //-----------------------------------------------
+            // PHASE 2: Sync from local to remote.
+            //-----------------------------------------------
+
+            // Do any remote lists need to be deleted?
+            List<Long> listIdsToDelete = new ArrayList<>();
+            listIdsToDelete.addAll(listIdsDeleted);
+            for (Long id : listIdsToDelete) {
+                L.d("Deleting remote list id " + id);
+                try {
+                    client.deleteList(getCsrfToken(wiki, csrfToken), id);
+                } catch (Throwable t) {
+                    L.w(t);
+                    if (!client.isServiceError(t) && 
!client.isUnavailableError(t)) {
+                        throw t;
+                    }
+                }
+                listIdsDeleted.remove(id);
+            }
+
+            // Do any remote pages need to be deleted?
+            List<String> pageIdsToDelete = new ArrayList<>();
+            pageIdsToDelete.addAll(pageIdsDeleted);
+            for (String id : pageIdsToDelete) {
+                L.d("Deleting remote page id " + id);
+                String[] listAndPageId = id.split(":");
+                try {
+                    // TODO: optimization opportunity once server starts 
supporting batch deletes.
+                    client.deletePageFromList(getCsrfToken(wiki, csrfToken), 
Long.parseLong(listAndPageId[0]), Long.parseLong(listAndPageId[1]));
+                } catch (Throwable t) {
+                    L.w(t);
+                    if (!client.isServiceError(t) && 
!client.isUnavailableError(t)) {
+                        throw t;
+                    }
+                }
+                pageIdsDeleted.remove(id);
+            }
+
+            // Determine whether any remote lists need to be created or updated
+            for (ReadingList localList : allLocalLists) {
+                RemoteReadingList remoteList =
+                        new RemoteReadingList(localList.title(), 
localList.description());
+
+                boolean upsertNeeded = false;
+                if (localList.remoteId() > 0) {
+                    if (!localList.isDefault() && localList.dirty()) {
+                        // Update remote metadata for this list.
+                        L.d("Updating info for remote list " + 
remoteList.name());
+                        client.updateList(getCsrfToken(wiki, csrfToken), 
localList.remoteId(), remoteList);
+                        upsertNeeded = true;
+                    }
+                } else if (!localList.isDefault()) {
+                    // This list needs to be created remotely.
+                    L.d("Creating remote list " + remoteList.name());
+                    long id = client.createList(getCsrfToken(wiki, csrfToken), 
remoteList);
+                    localList.remoteId(id);
+                    upsertNeeded = true;
+                }
+                if (upsertNeeded) {
+                    localList.dirty(false);
+                    ReadingListDbHelper.instance().updateList(localList, 
false);
+                }
+            }
+
+            for (ReadingList localList : allLocalLists) {
+                for (ReadingListPage localPage : localList.pages()) {
+                    if (localPage.remoteId() < 1) {
+                        L.d("Creating new remote page " + localPage.title());
+                        RemoteReadingListEntry entryForRemote = 
remoteEntryFromLocalPage(localPage);
+                        try {
+                            // TODO: optimization opportunity once server 
starts supporting batch adds.
+                            
localPage.remoteId(client.addPageToList(getCsrfToken(wiki, csrfToken), 
localList.remoteId(), entryForRemote));
+                        } catch (Throwable t) {
+                            // TODO: optimization opportunity -- if the server 
can return the ID
+                            // of the existing page, then we wouldn't need to 
do a hard sync.
+
+                            // If the page already exists in the remote list, 
this means that
+                            // the contents of this list have diverged from 
the remote list,
+                            // so let's force a full sync.
+                            if (client.isErrorType(t, "duplicate-page")) {
+                                shouldRetryWithForce = true;
+                                break;
+                            } else {
+                                throw t;
+                            }
+                        }
+                        ReadingListDbHelper.instance().updatePage(localPage);
+                    }
+                }
+            }
+
+        } catch (Throwable t) {
+            /*
+            // In case we want to automatically setup lists for the user:
+            if (client.isErrorType(t, "not-set-up")) {
+                try {
+                    L.d("Setting up remote reading lists...");
+                    client.setup(getCsrfToken(wiki, csrfToken));
+                    shouldRetry = true;
+                } catch (Throwable caught) {
+                    t = caught;
+                }
+            }
+            */
+            if (client.isErrorType(t, "notloggedin")) {
+                try {
+                    L.d("Server doesn't believe we're logged in, so logging 
in...");
+                    getCsrfToken(wiki, csrfToken);
+                    shouldRetry = true;
+                } catch (Throwable caught) {
+                    t = caught;
+                }
+            }
+            L.w(t);
+        } finally {
+            lastSyncTime = getLastDateFromHeader(lastSyncTime, client);
+
+            Prefs.setReadingListsLastSyncTime(lastSyncTime);
+            Prefs.setReadingListsDeletedIds(listIdsDeleted);
+            Prefs.setReadingListPagesDeletedIds(pageIdsDeleted);
+
+            if (shouldSendSyncEvent) {
+                SavedPageSyncService.sendSyncEvent();
+            }
+            if ((shouldRetry || shouldRetryWithForce) && 
!extras.containsKey(SYNC_EXTRAS_RETRYING)) {
+                Bundle b = new Bundle();
+                b.putAll(extras);
+                b.putBoolean(SYNC_EXTRAS_RETRYING, true);
+                if (shouldRetryWithForce) {
+                    b.putBoolean(SYNC_EXTRAS_FORCE_FULL_SYNC, true);
+                }
+                manualSync(b);
+            }
+            L.d("Finished sync of reading lists.");
+        }
+    }
+
+
+    private String getCsrfToken(@NonNull WikiSite wiki, @NonNull List<String> 
tokenList) throws Throwable {
+        if (tokenList.size() == 0) {
+            tokenList.add(new CsrfTokenClient(wiki, wiki).getTokenBlocking());
+        }
+        return tokenList.get(0);
+    }
+
+    @NonNull
+    private String getLastDateFromHeader(@NonNull String lastSyncTime, 
@NonNull ReadingListClient client) {
+        String lastDateHeader = client.getLastDateHeader();
+        if (TextUtils.isEmpty(lastDateHeader)) {
+            return lastSyncTime;
+        }
+        try {
+            Date date = DateUtil.getHttpLastModifiedDate(lastDateHeader);
+            return DateUtil.getIso8601DateFormat().format(date);
+        } catch (ParseException e) {
+            return lastSyncTime;
+        }
+    }
+
+    private void createOrUpdatePage(@NonNull ReadingList listForPage,
+                                    @NonNull RemoteReadingListEntry 
remotePage) {
+        PageTitle remoteTitle = pageTitleFromRemoteEntry(remotePage);
+        ReadingListPage localPage = null;
+        boolean updateOnly = false;
+
+        for (ReadingListPage page : listForPage.pages()) {
+            if (ReadingListPage.toPageTitle(page).equals(remoteTitle)) {
+                localPage = page;
+                updateOnly = true;
+                break;
+            }
+        }
+        if (localPage == null) {
+            localPage = new 
ReadingListPage(pageTitleFromRemoteEntry(remotePage));
+            localPage.listId(listForPage.id());
+            if (ReadingListDbHelper.instance().pageExistsInList(listForPage, 
remoteTitle)) {
+                updateOnly = true;
+            }
+        }
+        localPage.remoteId(remotePage.id());
+        if (remotePage.summary() != null) {
+            localPage.description(remotePage.summary().getDescription());
+            localPage.thumbUrl(remotePage.summary().getThumbnailUrl());
+        }
+        if (updateOnly) {
+            L.d("Updating local page " + localPage.title());
+            ReadingListDbHelper.instance().updatePage(localPage);
+        } else {
+            L.d("Creating local page " + localPage.title());
+            ReadingListDbHelper.instance().addPagesToList(listForPage, 
Collections.singletonList(localPage), false);
+        }
+    }
+
+    private void deletePageByTitle(@NonNull ReadingList listForPage, @NonNull 
PageTitle title) {
+        ReadingListPage localPage = null;
+        for (ReadingListPage page : listForPage.pages()) {
+            if (ReadingListPage.toPageTitle(page).equals(title)) {
+                localPage = page;
+                break;
+            }
+        }
+        if (localPage == null) {
+            localPage = 
ReadingListDbHelper.instance().getPageByTitle(listForPage, title);
+            if (localPage == null) {
+                return;
+            }
+        }
+        L.d("Deleting local page " + localPage.title());
+        ReadingListDbHelper.instance().markPagesForDeletion(listForPage,
+                Collections.singletonList(localPage), false);
+    }
+
+    private PageTitle pageTitleFromRemoteEntry(@NonNull RemoteReadingListEntry 
remoteEntry) {
+        WikiSite wiki = new WikiSite(remoteEntry.project());
+        return new PageTitle(remoteEntry.title(), wiki);
+    }
+
+    private RemoteReadingListEntry remoteEntryFromLocalPage(@NonNull 
ReadingListPage localPage) {
+        PageTitle title = ReadingListPage.toPageTitle(localPage);
+        return new RemoteReadingListEntry(title.getWikiSite().scheme() + "://" 
+ title.getWikiSite().authority(), title.getPrefixedText());
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncService.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncService.java
new file mode 100644
index 0000000..7dbd47d
--- /dev/null
+++ 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncService.java
@@ -0,0 +1,29 @@
+package org.wikipedia.readinglist.sync;
+
+import android.app.Service;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.Intent;
+import android.os.IBinder;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+public class ReadingListSyncService extends Service {
+    @NonNull private static final Object SYNC_ADAPTER_LOCK = new Object();
+    @Nullable private static AbstractThreadedSyncAdapter SYNC_ADAPTER;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        synchronized (SYNC_ADAPTER_LOCK) {
+            if (SYNC_ADAPTER == null) {
+                SYNC_ADAPTER = new 
ReadingListSyncAdapter(getApplicationContext(), true);
+            }
+        }
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return SYNC_ADAPTER == null ? null : 
SYNC_ADAPTER.getSyncAdapterBinder();
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java
new file mode 100644
index 0000000..a4c51c5
--- /dev/null
+++ b/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java
@@ -0,0 +1,133 @@
+package org.wikipedia.readinglist.sync;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+import org.apache.commons.lang3.StringUtils;
+import org.wikipedia.dataclient.restbase.page.RbPageSummary;
+import org.wikipedia.json.annotations.Required;
+
+import java.util.List;
+
+public class SyncedReadingLists {
+
+    @SuppressWarnings("unused,NullableProblems") @Nullable private 
List<RemoteReadingList> lists;
+    @SuppressWarnings("unused,NullableProblems") @Nullable private 
List<RemoteReadingListEntry> entries;
+    @SuppressWarnings("unused,NullableProblems") @Nullable private String next;
+
+    public SyncedReadingLists() { }
+
+    public SyncedReadingLists(@NonNull List<RemoteReadingList> lists, @NonNull 
List<RemoteReadingListEntry> entries) {
+        this.lists = lists;
+        this.entries = entries;
+    }
+
+    @Nullable public List<RemoteReadingList> getLists() {
+        return lists;
+    }
+
+    @Nullable public List<RemoteReadingListEntry> getEntries() {
+        return entries;
+    }
+
+    @Nullable public String getContinueStr() {
+        return next;
+    }
+
+    public static class RemoteReadingList {
+        @SuppressWarnings("unused") @Required private long id;
+        @SuppressWarnings("unused") @SerializedName("default") private boolean 
isDefault;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String name;
+        @SuppressWarnings("unused") @Nullable private String description;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String created;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String updated;
+        @SuppressWarnings("unused") private boolean deleted;
+
+        public RemoteReadingList() { }
+
+        public RemoteReadingList(@NonNull String name, @Nullable String 
description) {
+            this.name = name;
+            this.description = description;
+        }
+
+        public long id() {
+            return id;
+        }
+
+        @NonNull public String name() {
+            return name;
+        }
+
+        @NonNull public String description() {
+            return StringUtils.defaultString(description);
+        }
+
+        public boolean isDefault() {
+            return isDefault;
+        }
+
+        public boolean isDeleted() {
+            return deleted;
+        }
+
+        @NonNull public String updatedDate() {
+            return updated;
+        }
+    }
+
+    public static class RemoteReadingListEntry {
+        @SuppressWarnings("unused") private long id;
+        @SuppressWarnings("unused") private long listId;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String project;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String title;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String created;
+        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String updated;
+        @SuppressWarnings("unused") @Nullable private RbPageSummary summary;
+        @SuppressWarnings("unused") private boolean deleted;
+
+        public RemoteReadingListEntry() { }
+
+        public RemoteReadingListEntry(@NonNull String project, @NonNull String 
title) {
+            this.project = project;
+            this.title = title;
+        }
+
+        public long id() {
+            return id;
+        }
+
+        public long listId() {
+            return listId;
+        }
+
+        @NonNull public String project() {
+            return project;
+        }
+
+        @NonNull public String title() {
+            return title;
+        }
+
+        @NonNull public String updatedDate() {
+            return updated;
+        }
+
+        @Nullable public RbPageSummary summary() {
+            return summary;
+        }
+
+        public boolean isDeleted() {
+            return deleted;
+        }
+    }
+
+    public class RemoteIdResponse {
+        @SuppressWarnings("unused") @Required private long id;
+
+        public long id() {
+            return id;
+        }
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java 
b/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
index 6c2729f..f0cfba6 100644
--- a/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
+++ b/app/src/main/java/org/wikipedia/savedpages/SavedPageSyncService.java
@@ -105,7 +105,7 @@
         }
     }
 
-    private void sendSyncEvent() {
+    public static void sendSyncEvent() {
         // Note: this method posts from a background thread but subscribers 
expect events to be
         // received on the main thread.
         WikipediaApp.getInstance().getBus().post(new ReadingListSyncEvent());
diff --git 
a/app/src/main/java/org/wikipedia/settings/DeveloperSettingsPreferenceLoader.java
 
b/app/src/main/java/org/wikipedia/settings/DeveloperSettingsPreferenceLoader.java
index fefa66f..97283b7 100644
--- 
a/app/src/main/java/org/wikipedia/settings/DeveloperSettingsPreferenceLoader.java
+++ 
b/app/src/main/java/org/wikipedia/settings/DeveloperSettingsPreferenceLoader.java
@@ -165,7 +165,7 @@
             PageTitle pageTitle = new PageTitle("" + (i + 1), 
WikipediaApp.getInstance().getWikiSite());
             pages.add(new ReadingListPage(pageTitle));
         }
-        ReadingListDbHelper.instance().addPagesToList(list, pages);
+        ReadingListDbHelper.instance().addPagesToList(list, pages, true);
     }
 
     private void setUpCookies(@NonNull PreferenceCategory cat) {
diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.java 
b/app/src/main/java/org/wikipedia/settings/Prefs.java
index 9fc5396..001bcb8 100644
--- a/app/src/main/java/org/wikipedia/settings/Prefs.java
+++ b/app/src/main/java/org/wikipedia/settings/Prefs.java
@@ -26,6 +26,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -511,6 +512,14 @@
         
setBoolean(R.string.preference_key_reading_lists_remote_delete_pending, 
pending);
     }
 
+    public static boolean isReadingListsRemoteSetupPending() {
+        return 
getBoolean(R.string.preference_key_reading_lists_remote_setup_pending, false);
+    }
+
+    public static void setReadingListsRemoteSetupPending(boolean pending) {
+        setBoolean(R.string.preference_key_reading_lists_remote_setup_pending, 
pending);
+    }
+
     public static boolean isInitialOnboardingEnabled() {
         return getBoolean(R.string.preference_key_initial_onboarding_enabled, 
true);
     }
@@ -623,5 +632,55 @@
         
setBoolean(R.string.preference_key_feed_customize_onboarding_card_enabled, 
enabled);
     }
 
+    public static String getReadingListsLastSyncTime() {
+        return getString(R.string.preference_key_reading_lists_last_sync_time, 
"");
+    }
+
+    public static void setReadingListsLastSyncTime(String timeStr) {
+        setString(R.string.preference_key_reading_lists_last_sync_time, 
timeStr);
+    }
+
+    @NonNull public static Set<Long> getReadingListsDeletedIds() {
+        Set<Long> set = new HashSet<>();
+        if (!contains(R.string.preference_key_reading_lists_deleted_ids)) {
+            return set;
+        }
+        //noinspection unchecked
+        Set<Long> tempSet = GsonUnmarshaller.unmarshal(new 
TypeToken<Set<Long>>(){},
+                getString(R.string.preference_key_reading_lists_deleted_ids, 
null));
+        if (tempSet != null) {
+            set.addAll(tempSet);
+        }
+        return set;
+    }
+
+    public static void setReadingListsDeletedIds(@NonNull Set<Long> set) {
+        Set<Long> currentSet = getReadingListsDeletedIds();
+        currentSet.addAll(set);
+        // TODO: constrain size?
+        setString(R.string.preference_key_reading_lists_deleted_ids, 
GsonMarshaller.marshal(set));
+    }
+
+    @NonNull public static Set<String> getReadingListPagesDeletedIds() {
+        Set<String> set = new HashSet<>();
+        if (!contains(R.string.preference_key_reading_lists_deleted_ids)) {
+            return set;
+        }
+        //noinspection unchecked
+        Set<String> tempSet = GsonUnmarshaller.unmarshal(new 
TypeToken<Set<String>>(){},
+                
getString(R.string.preference_key_reading_list_pages_deleted_ids, null));
+        if (tempSet != null) {
+            set.addAll(tempSet);
+        }
+        return set;
+    }
+
+    public static void setReadingListPagesDeletedIds(@NonNull Set<String> set) 
{
+        Set<String> currentSet = getReadingListPagesDeletedIds();
+        currentSet.addAll(set);
+        // TODO: constrain size?
+        setString(R.string.preference_key_reading_list_pages_deleted_ids, 
GsonMarshaller.marshal(set));
+    }
+
     private Prefs() { }
 }
diff --git 
a/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java 
b/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java
index fc361b6..f6491c9 100644
--- a/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java
+++ b/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java
@@ -12,6 +12,7 @@
 import org.wikipedia.R;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.activity.BaseActivity;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.theme.ThemeFittingRoomActivity;
 import org.wikipedia.util.ReleaseUtil;
 import org.wikipedia.util.StringUtil;
@@ -135,7 +136,9 @@
             if (newValue == Boolean.TRUE) {
                 ((SwitchPreferenceCompat) preference).setChecked(true);
                 Prefs.setReadingListSyncEnabled(true);
-                // TODO: kick off initial sync
+                Prefs.setReadingListsRemoteSetupPending(true);
+                Prefs.setReadingListsRemoteDeletePending(false);
+                ReadingListSyncAdapter.manualSync();
             } else {
                 new AlertDialog.Builder(getActivity())
                         
.setMessage(R.string.reading_lists_confirm_remote_delete)
@@ -167,8 +170,9 @@
         @Override public void onClick(DialogInterface dialog, int which) {
             ((SwitchPreferenceCompat) preference).setChecked(false);
             Prefs.setReadingListSyncEnabled(false);
+            Prefs.setReadingListsRemoteSetupPending(false);
             Prefs.setReadingListsRemoteDeletePending(true);
-            // TODO: kick off sync
+            ReadingListSyncAdapter.manualSync();
         }
     }
 
@@ -182,7 +186,9 @@
         @Override public void onClick(DialogInterface dialog, int which) {
             ((SwitchPreferenceCompat) preference).setChecked(true);
             Prefs.setReadingListSyncEnabled(true);
+            Prefs.setReadingListsRemoteSetupPending(true);
             Prefs.setReadingListsRemoteDeletePending(false);
+            ReadingListSyncAdapter.manualSync();
         }
     }
 }
diff --git a/app/src/main/res/layout/fragment_reading_lists.xml 
b/app/src/main/res/layout/fragment_reading_lists.xml
index 0a525e5..4aeaf5d 100644
--- a/app/src/main/res/layout/fragment_reading_lists.xml
+++ b/app/src/main/res/layout/fragment_reading_lists.xml
@@ -1,82 +1,90 @@
 <?xml version="1.0" encoding="utf-8"?>
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android";
+<android.support.v4.widget.SwipeRefreshLayout
+    xmlns:android="http://schemas.android.com/apk/res/android";
     xmlns:app="http://schemas.android.com/apk/res-auto";
+    android:id="@+id/reading_list_swipe_refresh"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:orientation="vertical">
+    android:layout_height="wrap_content">
 
     <LinearLayout
-        android:id="@+id/reading_list_content_container"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:animateLayoutChanges="true"
+        android:layout_height="match_parent"
         android:orientation="vertical">
 
-        <FrameLayout
-            android:id="@+id/reading_list_onboarding_container"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content" />
-
-        <android.support.v7.widget.RecyclerView
-            android:id="@+id/reading_list_list"
+        <LinearLayout
+            android:id="@+id/reading_list_content_container"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:scrollbars="vertical" />
+            android:animateLayoutChanges="true"
+            android:orientation="vertical">
+
+            <FrameLayout
+                android:id="@+id/reading_list_onboarding_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+            <android.support.v7.widget.RecyclerView
+                android:id="@+id/reading_list_list"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:scrollbars="vertical" />
+
+        </LinearLayout>
+
+        <ScrollView
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:fillViewport="true"
+            android:layout_weight="1">
+
+            <LinearLayout
+                android:id="@+id/empty_container"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_marginLeft="30dp"
+                android:layout_marginRight="30dp"
+                android:gravity="center"
+                android:layout_gravity="center_horizontal"
+                android:orientation="vertical"
+                android:visibility="gone">
+
+                <TextView
+                    android:id="@+id/empty_title"
+                    style="@style/MaterialLargePrimaryTitle"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal"
+                    android:layout_marginBottom="20dp"
+                    android:gravity="center"
+                    android:text="@string/reading_lists_empty" />
+
+                <ImageView
+                    android:id="@+id/empty_image"
+                    android:layout_width="168dp"
+                    android:layout_height="168dp"
+                    android:layout_gravity="center_horizontal"
+                    android:contentDescription="@null"
+                    app:srcCompat="@drawable/no_lists" />
+
+                <TextView
+                    android:id="@+id/empty_message"
+                    style="@style/MaterialMediumSecondaryCaption"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="32dp"
+                    android:layout_gravity="center_horizontal"
+                    android:gravity="center"
+                    android:text="@string/reading_lists_empty_message" />
+            </LinearLayout>
+        </ScrollView>
+
+        <org.wikipedia.views.SearchEmptyView
+            android:id="@+id/search_empty_view"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center" />
 
     </LinearLayout>
 
-    <ScrollView
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:fillViewport="true"
-        android:layout_weight="1">
-
-        <LinearLayout
-            android:id="@+id/empty_container"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:layout_marginLeft="30dp"
-            android:layout_marginRight="30dp"
-            android:gravity="center"
-            android:layout_gravity="center_horizontal"
-            android:orientation="vertical"
-            android:visibility="gone">
-
-            <TextView
-                android:id="@+id/empty_title"
-                style="@style/MaterialLargePrimaryTitle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_gravity="center_horizontal"
-                android:layout_marginBottom="20dp"
-                android:gravity="center"
-                android:text="@string/reading_lists_empty" />
-
-            <ImageView
-                android:id="@+id/empty_image"
-                android:layout_width="168dp"
-                android:layout_height="168dp"
-                android:layout_gravity="center_horizontal"
-                android:contentDescription="@null"
-                app:srcCompat="@drawable/no_lists" />
-
-            <TextView
-                android:id="@+id/empty_message"
-                style="@style/MaterialMediumSecondaryCaption"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="32dp"
-                android:layout_gravity="center_horizontal"
-                android:gravity="center"
-                android:text="@string/reading_lists_empty_message" />
-        </LinearLayout>
-    </ScrollView>
-
-    <org.wikipedia.views.SearchEmptyView
-        android:id="@+id/search_empty_view"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center" />
-
-</LinearLayout>
+</android.support.v4.widget.SwipeRefreshLayout>
\ No newline at end of file
diff --git a/app/src/main/res/values/preference_keys.xml 
b/app/src/main/res/values/preference_keys.xml
index 9fe5b32..7880742 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -54,6 +54,7 @@
     <string name="preference_key_sync_reading_lists">syncReadingLists</string>
     <string 
name="preference_key_reading_list_sync_reminder_enabled">readingListSyncReminder</string>
     <string 
name="preference_key_reading_list_login_reminder_enabled">readingListLoginReminder</string>
+    <string 
name="preference_key_reading_lists_remote_setup_pending">readingListsRemoteSetupPending</string>
     <string 
name="preference_key_reading_lists_remote_delete_pending">readingListsRemoteDeletePending</string>
     <string 
name="preference_key_initial_onboarding_enabled">initialOnboardingEnabled</string>
     <string 
name="preference_key_reading_lists_current_user_hash">readingListsCurrentUserHash</string>
@@ -70,4 +71,7 @@
     <string 
name="preference_key_feed_customize_onboarding_card_enabled">feedCustomizeOnboardingCardEnabled</string>
     <string name="preference_key_add_articles">addArticles</string>
     <string name="preference_key_add_reading_lists">addReadingLists</string>
+    <string 
name="preference_key_reading_lists_last_sync_time">readingListsLastSyncTime</string>
+    <string 
name="preference_key_reading_lists_deleted_ids">readingListsDeletedIds</string>
+    <string 
name="preference_key_reading_list_pages_deleted_ids">readingListPagesDeletedIds</string>
 </resources>
diff --git a/app/src/main/res/xml/reading_list_sync_adapter.xml 
b/app/src/main/res/xml/reading_list_sync_adapter.xml
new file mode 100644
index 0000000..e8a40ab
--- /dev/null
+++ b/app/src/main/res/xml/reading_list_sync_adapter.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<sync-adapter
+    xmlns:android="http://schemas.android.com/apk/res/android";
+    android:contentAuthority="@string/reading_lists_authority"
+    android:accountType="@string/account_type"
+    android:supportsUploading="true"
+    android:allowParallelSyncs="false"
+    android:isAlwaysSyncable="true" />
\ No newline at end of file

-- 
To view, visit https://gerrit.wikimedia.org/r/391048
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ia82c4c5288c28ae83f7a2d56d603cae14e86adb2
Gerrit-PatchSet: 22
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Dbrant <dbr...@wikimedia.org>
Gerrit-Reviewer: Brion VIBBER <br...@wikimedia.org>
Gerrit-Reviewer: Cooltey <cf...@wikimedia.org>
Gerrit-Reviewer: Dbrant <dbr...@wikimedia.org>
Gerrit-Reviewer: Sharvaniharan <sha...@wikimedia.org>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to