Dbrant has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/391049 )

Change subject: [VERY WIP] Integrate with Reading List service.
......................................................................

[VERY WIP] Integrate with Reading List service.

Change-Id: Ib168adf7bb5149f2e0c82ab0ffb8ba89341ab153
---
M app/src/main/java/org/wikipedia/activity/BaseActivity.java
M app/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java
A app/src/main/java/org/wikipedia/database/ReadingListsContentProvider.java
M app/src/main/java/org/wikipedia/database/http/HttpRowDao.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/login/LoginClient.java
M app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListData.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListPageDetailFetcher.java
M app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
M app/src/main/java/org/wikipedia/readinglist/database/ReadingListRow.java
M app/src/main/java/org/wikipedia/readinglist/database/ReadingListTable.java
M app/src/main/java/org/wikipedia/readinglist/page/ReadingListPageRow.java
M 
app/src/main/java/org/wikipedia/readinglist/page/database/ReadingListPageDao.java
M app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
A app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.java
D app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSynchronizer.java
M app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.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/java/org/wikipedia/useroption/database/UserOptionDao.java
M app/src/main/res/values/preference_keys.xml
A app/src/main/res/xml/reading_list_sync_adapter.xml
26 files changed, 887 insertions(+), 382 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/apps/android/wikipedia 
refs/changes/49/391049/1

diff --git a/app/src/main/java/org/wikipedia/activity/BaseActivity.java 
b/app/src/main/java/org/wikipedia/activity/BaseActivity.java
index 0e805cf..5490084 100644
--- a/app/src/main/java/org/wikipedia/activity/BaseActivity.java
+++ b/app/src/main/java/org/wikipedia/activity/BaseActivity.java
@@ -29,7 +29,7 @@
 import org.wikipedia.events.WikipediaZeroEnterEvent;
 import org.wikipedia.offline.Compilation;
 import org.wikipedia.offline.OfflineManager;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.recurring.RecurringTasksExecutor;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.util.DeviceUtil;
@@ -198,7 +198,7 @@
         public void onReceive(Context context, Intent intent) {
             if (DeviceUtil.isOnline()) {
                 onGoOnline();
-                ReadingListSynchronizer.instance().syncSavedPages();
+                ReadingListSyncAdapter.syncSavedPages();
             } else {
                 onGoOffline();
             }
@@ -220,7 +220,7 @@
         }
 
         @Subscribe public void on(NetworkConnectEvent event) {
-            ReadingListSynchronizer.instance().syncSavedPages();
+            ReadingListSyncAdapter.syncSavedPages();
         }
 
         @Subscribe public void on(ThemeChangeEvent event) {
diff --git a/app/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java 
b/app/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java
index 5ee0b88..37aeb9d 100644
--- a/app/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java
+++ b/app/src/main/java/org/wikipedia/csrf/CsrfTokenClient.java
@@ -33,7 +33,7 @@
 
 
 
-    // TODO!!!!!!!!!!!!!! Restore to uncommented version when ready.
+    // TODO!!!!!!!!!!!!!! Restore commented version when ready.
     //@NonNull private final WikiCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
     @NonNull private final WikiCachedService<Service> cachedService = new 
MwCachedService<Service>(Service.class) {
         @NonNull @Override protected Retrofit create() {
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/database/http/HttpRowDao.java 
b/app/src/main/java/org/wikipedia/database/http/HttpRowDao.java
index 5d04441..af4ad9f 100644
--- a/app/src/main/java/org/wikipedia/database/http/HttpRowDao.java
+++ b/app/src/main/java/org/wikipedia/database/http/HttpRowDao.java
@@ -15,7 +15,7 @@
     }
 
     // TODO: most clients just have a Dat. Should the input be that instead?
-    public synchronized void markUpserted(@NonNull Row row) {
+    public synchronized void markModified(@NonNull Row row) {
         Row query = queryPrimaryKey(row);
         switch (query == null ? HttpStatus.DELETED : query.status()) {
             case SYNCHRONIZED:
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 ac27029..a161eac 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
@@ -24,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() {
@@ -61,6 +62,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 4269965..028743b 100644
--- a/app/src/main/java/org/wikipedia/feed/FeedFragment.java
+++ b/app/src/main/java/org/wikipedia/feed/FeedFragment.java
@@ -39,7 +39,7 @@
 import org.wikipedia.history.HistoryEntry;
 import org.wikipedia.offline.LocalCompilationsActivity;
 import org.wikipedia.offline.OfflineTutorialActivity;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.settings.SettingsActivity;
 import org.wikipedia.util.FeedbackUtil;
@@ -157,7 +157,7 @@
             getCallback().updateToolbarElevation(shouldElevateToolbar());
         }
 
-        ReadingListSynchronizer.instance().sync();
+        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 5960daa..ea583e4 100644
--- a/app/src/main/java/org/wikipedia/login/LoginActivity.java
+++ b/app/src/main/java/org/wikipedia/login/LoginActivity.java
@@ -23,7 +23,7 @@
 import org.wikipedia.auth.AccountUtil;
 import org.wikipedia.createaccount.CreateAccountActivity;
 import org.wikipedia.page.PageTitle;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.util.log.L;
 import org.wikipedia.views.NonEmptyValidator;
@@ -242,7 +242,7 @@
                     hideSoftKeyboard(LoginActivity.this);
                     setResult(RESULT_LOGIN_SUCCESS);
 
-                    ReadingListSynchronizer.instance().sync();
+                    ReadingListSyncAdapter.manualSync();
                     finish();
                 } else if (result.fail()) {
                     String message = result.getMessage();
diff --git a/app/src/main/java/org/wikipedia/login/LoginClient.java 
b/app/src/main/java/org/wikipedia/login/LoginClient.java
index 359cc86..47c61dc 100644
--- a/app/src/main/java/org/wikipedia/login/LoginClient.java
+++ b/app/src/main/java/org/wikipedia/login/LoginClient.java
@@ -40,7 +40,7 @@
 
 
 
-    // TODO!!!!!!!!!!!!!! Restore to uncommented version when ready.
+    // TODO!!!!!!!!!!!!!! Restore commented version when ready.
     //@NonNull private final WikiCachedService<Service> cachedService = new 
MwCachedService<>(Service.class);
     @NonNull private final WikiCachedService<Service> cachedService = new 
MwCachedService<Service>(Service.class) {
         @NonNull @Override protected Retrofit create() {
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java 
b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
index 4f169f3..b14202a 100644
--- a/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
+++ b/app/src/main/java/org/wikipedia/readinglist/AddToReadingListDialog.java
@@ -23,7 +23,7 @@
 import org.wikipedia.page.PageTitle;
 import org.wikipedia.readinglist.page.ReadingListPage;
 import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.util.DimenUtil;
 import org.wikipedia.util.FeedbackUtil;
@@ -242,7 +242,7 @@
                     }
                     showViewListSnackBar(readingList, message);
                     ReadingList.DAO.addTitleToList(readingList, page, false);
-                    ReadingListSynchronizer.instance().bumpRevAndSync();
+                    ReadingListSyncAdapter.manualSync();
                     dismiss();
                 }
             }
@@ -277,7 +277,7 @@
                     for (String key : result) {
                         ReadingList.DAO.addTitleToList(readingList, 
pages.get(key), false);
                     }
-                    ReadingListSynchronizer.instance().bumpRevAndSync();
+                    ReadingListSyncAdapter.manualSync();
                     dismiss();
                 }
             }
diff --git a/app/src/main/java/org/wikipedia/readinglist/ReadingListData.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListData.java
index 3432840..ee8eab5 100644
--- a/app/src/main/java/org/wikipedia/readinglist/ReadingListData.java
+++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListData.java
@@ -28,6 +28,20 @@
                               @NonNull 
CallbackTask.Callback<List<ReadingList>> callback) {
         CallbackTask.execute(new CallbackTask.Task<List<ReadingList>>() {
             @Override public List<ReadingList> execute() {
+                List<ReadingList> result = queryMruLists(searchQuery);
+
+                // TODO: enable default list when ready.
+                ReadingList defaultList = null;
+                for (ReadingList list : result) {
+                    if (list.remoteId() > 0 && 
list.getTitle().equals("default")) {
+                        defaultList = list;
+                        break;
+                    }
+                }
+                if (defaultList != null) {
+                    result.remove(defaultList);
+                }
+
                 return queryMruLists(searchQuery);
             }
         }, callback);
@@ -171,13 +185,13 @@
         for (ReadingListPage page : pages) {
             page.removeListKey(oldKey);
             page.addListKey(list.key());
-            ReadingListPageDao.instance().upsert(page);
+            ReadingListPageDao.instance().upsert(page, false);
         }
     }
 
     public synchronized void setPageOffline(@NonNull ReadingListPage page, 
boolean offline) {
         page.setOffline(offline);
-        ReadingListPageDao.instance().upsert(page);
+        ReadingListPageDao.instance().upsert(page, false);
     }
 
     public synchronized void removeList(@NonNull ReadingList list) {
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
index 0c6745d..9e4fef9 100644
--- a/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
+++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListFragment.java
@@ -44,8 +44,8 @@
 import org.wikipedia.readinglist.page.ReadingListPage;
 import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
 import org.wikipedia.readinglist.page.database.ReadingListPageDao;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.readinglist.sync.ReadingListSyncEvent;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.util.FeedbackUtil;
 import org.wikipedia.util.ShareUtil;
@@ -332,7 +332,7 @@
             public void onClick(View v) {
                 for (ReadingListPage page : pages) {
                     ReadingList.DAO.addTitleToList(readingList, page, true);
-                    ReadingListSynchronizer.instance().bumpRevAndSync();
+                    ReadingListSyncAdapter.manualSync();
                     ReadingListPageDao.instance().markOutdated(page);
                 }
                 update();
@@ -352,7 +352,7 @@
                     public void onSuccess(@NonNull CharSequence text) {
                         readingListTitle = text.toString();
                         ReadingList.DAO.renameAndSaveListInfo(readingList, 
readingListTitle);
-                        ReadingListSynchronizer.instance().bumpRevAndSync();
+                        
ReadingListSyncAdapter.manualSyncWithListUpdate(readingList);
                         update();
                         funnel.logModifyList(readingList, readingLists.size());
                     }
@@ -374,7 +374,7 @@
             public void onSuccess(@NonNull CharSequence text) {
                 readingList.setDescription(text.toString());
                 ReadingList.DAO.saveListInfo(readingList);
-                ReadingListSynchronizer.instance().bumpRevAndSync();
+                ReadingListSyncAdapter.manualSyncWithListUpdate(readingList);
                 update();
                 funnel.logModifyList(readingList, readingLists.size());
             }
@@ -451,7 +451,7 @@
             for (ReadingListPage page : selectedPages) {
                 ReadingList.DAO.removeTitleFromList(readingList, page);
             }
-            ReadingListSynchronizer.instance().bumpRevAndSync();
+            ReadingListSyncAdapter.manualSync();
             funnel.logDeleteItem(readingList, readingLists.size());
             showDeleteItemsUndoSnackbar(readingList, selectedPages);
             update();
@@ -465,7 +465,7 @@
                     ReadingListData.instance().setPageOffline(page, false);
                 }
             }
-            ReadingListSynchronizer.instance().sync();
+            ReadingListSyncAdapter.syncSavedPages();
             showMultiSelectOfflineStateChangeSnackbar(selectedPages, false);
             adapter.notifyDataSetChanged();
             update();
@@ -479,7 +479,7 @@
                     ReadingListData.instance().setPageOffline(page, true);
                 }
             }
-            ReadingListSynchronizer.instance().sync();
+            ReadingListSyncAdapter.syncSavedPages();
             showMultiSelectOfflineStateChangeSnackbar(selectedPages, true);
             adapter.notifyDataSetChanged();
             update();
@@ -506,7 +506,7 @@
         }
         showDeleteItemsUndoSnackbar(readingList, 
Collections.singletonList(page));
         ReadingList.DAO.removeTitleFromList(readingList, page);
-        ReadingListSynchronizer.instance().bumpRevAndSync();
+        ReadingListSyncAdapter.manualSync();
         funnel.logDeleteItem(readingList, readingLists.size());
         update();
     }
@@ -563,7 +563,7 @@
                     ? 
getQuantityString(R.plurals.reading_list_article_offline_message, 1)
                     : 
getQuantityString(R.plurals.reading_list_article_not_offline_message, 1));
             adapter.notifyDataSetChanged();
-            ReadingListSynchronizer.instance().syncSavedPages();
+            ReadingListSyncAdapter.syncSavedPages();
         }
     }
 
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListPageDetailFetcher.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListPageDetailFetcher.java
index bab91e9..035607e 100644
--- 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListPageDetailFetcher.java
+++ 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListPageDetailFetcher.java
@@ -101,7 +101,7 @@
                 if ((isFromRequestWiki(page)) && 
resultMap.containsKey(page.title())) {
                     
page.setThumbnailUrl(resultMap.get(page.title()).thumbUrl());
                     
page.setDescription(resultMap.get(page.title()).description());
-                    ReadingListPageDao.instance().upsert(page);
+                    ReadingListPageDao.instance().upsert(page, false);
                 }
             }
             callback.success();
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java 
b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
index d1dae74..7afb99f 100644
--- a/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
+++ b/app/src/main/java/org/wikipedia/readinglist/ReadingListsFragment.java
@@ -31,8 +31,8 @@
 import org.wikipedia.history.SearchActionModeCallback;
 import org.wikipedia.onboarding.OnboardingView;
 import org.wikipedia.readinglist.page.ReadingListPage;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.readinglist.sync.ReadingListSyncEvent;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.settings.SettingsActivity;
 import org.wikipedia.util.FeedbackUtil;
@@ -275,7 +275,7 @@
                         public void onSuccess(@NonNull CharSequence text) {
                             ReadingList.DAO.renameAndSaveListInfo(readingList, 
text.toString());
                             updateLists();
-                            
ReadingListSynchronizer.instance().bumpRevAndSync();
+                            
ReadingListSyncAdapter.manualSyncWithListUpdate(readingList);
                             funnel.logModifyList(readingList, 
readingLists.size());
                         }
                     }).show();
@@ -295,7 +295,7 @@
                     readingList.setDescription(text.toString());
                     ReadingList.DAO.saveListInfo(readingList);
                     updateLists();
-                    ReadingListSynchronizer.instance().bumpRevAndSync();
+                    
ReadingListSyncAdapter.manualSyncWithListUpdate(readingList);
                     funnel.logModifyList(readingList, readingLists.size());
                 }
             }).show();
@@ -313,7 +313,7 @@
                     ReadingListData.instance().setPageOffline(page, true);
                 }
             }
-            ReadingListSynchronizer.instance().sync();
+            ReadingListSyncAdapter.syncSavedPages();
             updateLists();
             showMultiSelectOfflineStateChangeSnackbar(readingList.getPages(), 
true);
         }
@@ -325,7 +325,7 @@
                     ReadingListData.instance().setPageOffline(page, false);
                 }
             }
-            ReadingListSynchronizer.instance().sync();
+            ReadingListSyncAdapter.syncSavedPages();
             updateLists();
             showMultiSelectOfflineStateChangeSnackbar(readingList.getPages(), 
false);
         }
@@ -351,7 +351,7 @@
         if (readingList != null) {
             showDeleteListUndoSnackbar(readingList);
             ReadingList.DAO.removeList(readingList);
-            ReadingListSynchronizer.instance().bumpRevAndSync();
+            ReadingListSyncAdapter.manualSyncWithListDelete(readingList);
             funnel.logDeleteList(readingList, readingLists.size());
             updateLists();
         }
@@ -364,8 +364,10 @@
         snackbar.setAction(R.string.reading_list_item_delete_undo, new 
View.OnClickListener() {
             @Override
             public void onClick(View v) {
+                // reset the remote ID of this list, because it will need to 
be recreated remotely.
+                readingList.remoteId(-1);
                 ReadingList.DAO.addList(readingList);
-                ReadingListSynchronizer.instance().bumpRevAndSync();
+                ReadingListSyncAdapter.manualSync();
                 updateLists();
             }
         });
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListRow.java 
b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListRow.java
index bcd4965..3e12e5e 100644
--- a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListRow.java
+++ b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListRow.java
@@ -3,6 +3,7 @@
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import org.apache.commons.lang3.StringUtils;
 import org.wikipedia.model.BaseModel;
 import org.wikipedia.readinglist.ReadingListData;
 import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
@@ -49,8 +50,8 @@
         this.atime = atime;
     }
 
-    @Nullable public String getDescription() {
-        return description;
+    @NonNull public String getDescription() {
+        return StringUtils.defaultString(description);
     }
 
     public void description(@Nullable String description) {
@@ -61,6 +62,10 @@
         return remoteId;
     }
 
+    public void remoteId(long remoteId) {
+        this.remoteId = remoteId;
+    }
+
     protected ReadingListRow(@NonNull Builder<?> builder) {
         key = builder.key;
         title = builder.title;
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListTable.java 
b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListTable.java
index 9be020a..0897424 100644
--- a/app/src/main/java/org/wikipedia/readinglist/database/ReadingListTable.java
+++ b/app/src/main/java/org/wikipedia/readinglist/database/ReadingListTable.java
@@ -12,7 +12,7 @@
 import org.wikipedia.database.contract.ReadingListContract;
 import org.wikipedia.readinglist.page.ReadingListPage;
 import org.wikipedia.readinglist.page.database.ReadingListPageDao;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.util.FileUtil;
 
 import java.io.File;
@@ -110,7 +110,7 @@
                 } finally {
                     c.close();
                 }
-                ReadingListSynchronizer.instance().syncSavedPages();
+                ReadingListSyncAdapter.manualSync();
                 return null;
             }
         }, null);
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/page/ReadingListPageRow.java 
b/app/src/main/java/org/wikipedia/readinglist/page/ReadingListPageRow.java
index 195b9d4..563de1c 100644
--- a/app/src/main/java/org/wikipedia/readinglist/page/ReadingListPageRow.java
+++ b/app/src/main/java/org/wikipedia/readinglist/page/ReadingListPageRow.java
@@ -6,6 +6,7 @@
 
 import com.google.gson.annotations.SerializedName;
 
+import org.apache.commons.lang3.StringUtils;
 import org.wikipedia.dataclient.WikiSite;
 import org.wikipedia.model.BaseModel;
 import org.wikipedia.page.Namespace;
@@ -96,8 +97,8 @@
         atime = System.currentTimeMillis();
     }
 
-    @Nullable public String thumbnailUrl() {
-        return thumbnailUrl;
+    @NonNull public String thumbnailUrl() {
+        return StringUtils.defaultString(thumbnailUrl);
     }
 
     public void setThumbnailUrl(@Nullable String thumbnailUrl) {
@@ -108,8 +109,8 @@
         this.description = description;
     }
 
-    @Nullable public String description() {
-        return description;
+    @NonNull public String description() {
+        return StringUtils.defaultString(description);
     }
 
     @Nullable public Long physicalSize() {
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/page/database/ReadingListPageDao.java
 
b/app/src/main/java/org/wikipedia/readinglist/page/database/ReadingListPageDao.java
index d9f7f15..3506105 100644
--- 
a/app/src/main/java/org/wikipedia/readinglist/page/database/ReadingListPageDao.java
+++ 
b/app/src/main/java/org/wikipedia/readinglist/page/database/ReadingListPageDao.java
@@ -102,11 +102,17 @@
     }
 
     public synchronized void upsert(@NonNull ReadingListPage row) {
+        upsert(row, true);
+    }
+
+    public synchronized void upsert(@NonNull ReadingListPage row, boolean 
queueForSync) {
         if (row.listKeys().isEmpty()) {
             httpDao.markDeleted(new ReadingListPageHttpRow(row));
             diskDao.markDeleted(new ReadingListPageDiskRow(row));
         } else {
-            httpDao.markUpserted(new ReadingListPageHttpRow(row));
+            if (queueForSync) {
+                httpDao.markModified(new ReadingListPageHttpRow(row));
+            }
             if (row.diskStatus() == DiskStatus.OUTDATED) {
                 diskDao.markOutdated(new ReadingListPageDiskRow(row));
             } else if (row.diskStatus() == DiskStatus.ONLINE || 
row.diskStatus() == DiskStatus.UNSAVED) {
@@ -142,6 +148,23 @@
         diskDao.failTransaction(row);
     }
 
+    @NonNull public synchronized Collection<ReadingListPageHttpRow> 
startHttpTransaction() {
+        Collection<ReadingListPageHttpRow> rows = 
queryPendingHttpTransactions();
+        httpDao.startTransaction(rows);
+        return rows;
+    }
+
+    public synchronized void completeHttpTransaction(@NonNull 
ReadingListPageHttpRow row) {
+        httpDao.completeTransaction(row);
+        if (row.dat() != null) {
+            super.upsert(row.dat());
+        }
+    }
+
+    public synchronized void failHttpTransaction(@NonNull 
ReadingListPageHttpRow row) {
+        httpDao.failTransaction(row);
+    }
+
     public void clearAsync() {
         CallbackTask.execute(new Task<Void>() {
             @Override public Void execute() {
@@ -162,21 +185,28 @@
         String selection = Sql.SELECT_ROWS_PENDING_DISK_TRANSACTION;
         final String[] selectionArgs = null;
         final String order = null;
-        Cursor cursor = client().select(uri, selection,
-                selectionArgs, order);
-
-        Collection<ReadingListPageDiskRow> rows = new ArrayList<>();
-        try {
+        try (Cursor cursor = client().select(uri, selection, selectionArgs, 
order)) {
+            Collection<ReadingListPageDiskRow> rows = new ArrayList<>();
             while (cursor.moveToNext()) {
                 rows.add(ReadingListPageDiskRow.fromCursor(cursor));
             }
-        } finally {
-            cursor.close();
+            return rows;
         }
-        return rows;
     }
 
-    // TODO: expose HTTP DAO methods.
+    @NonNull private Collection<ReadingListPageHttpRow> 
queryPendingHttpTransactions() {
+        Uri uri = ReadingListPageContract.HttpWithPage.URI;
+        String selection = Sql.SELECT_ROWS_PENDING_HTTP_TRANSACTION;
+        final String[] selectionArgs = null;
+        final String order = null;
+        try (Cursor cursor = client().select(uri, selection, selectionArgs, 
order)) {
+            Collection<ReadingListPageHttpRow> rows = new ArrayList<>();
+            while (cursor.moveToNext()) {
+                rows.add(ReadingListPageHttpRow.fromCursor(cursor));
+            }
+            return rows;
+        }
+    }
 
     private ReadingListPageDao() {
         
super(WikipediaApp.getInstance().getDatabaseClient(ReadingListPageRow.class));
@@ -203,8 +233,12 @@
         private static final String SELECT_ROWS_WITH_LIST_KEY = "',' || 
:listKeyCol || ',' like '%,' || ? || ',%'"
             .replaceAll(":listKeyCol", 
ReadingListPageContract.Page.LIST_KEYS.qualifiedName());
 
-        private static String SELECT_ROWS_PENDING_DISK_TRANSACTION = 
":transactionIdCol == :noTransactionId"
+        private static final String SELECT_ROWS_PENDING_DISK_TRANSACTION = 
":transactionIdCol == :noTransactionId"
             .replaceAll(":transactionIdCol", 
ReadingListPageContract.DiskWithPage.DISK_TRANSACTION_ID.qualifiedName())
             .replaceAll(":noTransactionId", 
String.valueOf(AsyncConstant.NO_TRANSACTION_ID));
+
+        private static final String SELECT_ROWS_PENDING_HTTP_TRANSACTION = 
":transactionIdCol == :noTransactionId"
+                .replaceAll(":transactionIdCol",  
ReadingListPageContract.HttpWithPage.HTTP_TRANSACTION_ID.qualifiedName())
+                .replaceAll(":noTransactionId", 
String.valueOf(AsyncConstant.NO_TRANSACTION_ID));
     }
 }
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
index 694a21b..68c2832 100644
--- a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
+++ b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListClient.java
@@ -13,6 +13,7 @@
 import org.wikipedia.dataclient.retrofit.RbCachedService;
 import org.wikipedia.dataclient.retrofit.RetrofitFactory;
 import org.wikipedia.dataclient.retrofit.WikiCachedService;
+import org.wikipedia.json.GsonMarshaller;
 import org.wikipedia.readinglist.ReadingList;
 import org.wikipedia.settings.Prefs;
 import org.wikipedia.util.log.L;
@@ -27,19 +28,26 @@
 import retrofit2.Callback;
 import retrofit2.Response;
 import retrofit2.Retrofit;
+import retrofit2.http.Body;
+import retrofit2.http.DELETE;
 import retrofit2.http.Field;
 import retrofit2.http.FormUrlEncoded;
 import retrofit2.http.GET;
 import retrofit2.http.Header;
 import retrofit2.http.Headers;
 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 {
 
 
 
-    // TODO!!!!!!!!!!!!!! Restore to uncommented version when ready.
+    // TODO!!!!!!!!!!!!!! Restore commented version when ready.
     //@NonNull private final WikiCachedService<Service> cachedService = new 
RbCachedService<>(Service.class);
     @NonNull private final WikiCachedService<Service> cachedService = new 
RbCachedService<Service>(Service.class) {
         @NonNull @Override protected Retrofit create() {
@@ -51,22 +59,17 @@
 
 
     @NonNull private final WikiSite wiki;
+    @Nullable private String lastDateHeader;
 
     public ReadingListClient(@NonNull WikiSite wiki) {
         this.wiki = wiki;
     }
 
-    public List<SyncedReadingLists.RemoteReadingList> 
getListsChangedSince(@NonNull String date) throws Throwable {
-        Response<SyncedReadingLists> response = 
cachedService.service(wiki).getListsChangedSince(date).execute();
-        SyncedReadingLists lists = response.body();
-        if (lists == null || lists.getLists() == null) {
-            throw new IOException("Incorrect response format.");
-        }
-        // TODO: check for "not-set-up" error
-        return lists.getLists();
+    @Nullable public String getLastDateHeader() {
+        return lastDateHeader;
     }
 
-    private void setup(@NonNull String csrfToken) throws Throwable {
+    public void setup(@NonNull String csrfToken) throws Throwable {
         try {
             cachedService.service(wiki).setup(csrfToken).execute();
         } catch (Throwable t) {
@@ -77,16 +80,106 @@
         }
     }
 
-    private List<SyncedReadingLists.RemoteReadingList> getRemoteLists() throws 
Throwable {
+    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;
+        }
+    }
+
+    public List<RemoteReadingList> getAllLists() throws Throwable {
         Response<SyncedReadingLists> response = 
cachedService.service(wiki).getLists().execute();
         SyncedReadingLists lists = response.body();
         if (lists == null || lists.getLists() == null) {
             throw new IOException("Incorrect response format.");
         }
-        // TODO: check for "not-set-up" error
+        saveLastDateHeader(response);
+        // TODO: implement continuation.
         return lists.getLists();
     }
 
+    public SyncedReadingLists getChangesSince(@NonNull String date) throws 
Throwable {
+        Response<SyncedReadingLists> response = 
cachedService.service(wiki).getChangesSince(date).execute();
+        SyncedReadingLists body = response.body();
+        if (body == null) {
+            throw new IOException("Incorrect response format.");
+        }
+        saveLastDateHeader(response);
+        // TODO: implement continuation
+        return body;
+    }
+
+    public List<RemoteReadingList> getListsContaining(RemoteReadingListEntry 
entry) throws Throwable {
+        Response<SyncedReadingLists> response = cachedService.service(wiki)
+                .getListsContaining(entry.project(), entry.title()).execute();
+        SyncedReadingLists lists = response.body();
+        if (lists == null || lists.getLists() == null) {
+            throw new IOException("Incorrect response format.");
+        }
+        saveLastDateHeader(response);
+        // TODO: implement continuation.
+        return lists.getLists();
+    }
+
+    public List<RemoteReadingListEntry> getListEntries(long listId) throws 
Throwable {
+        Response<SyncedReadingLists.RemoteEntryCollection> response
+                = cachedService.service(wiki).getListEntries(listId).execute();
+        SyncedReadingLists.RemoteEntryCollection collection = response.body();
+        if (collection == null || collection.getEntries() == null) {
+            throw new IOException("Incorrect response format.");
+        }
+        saveLastDateHeader(response);
+        // TODO: implement continuation.
+        return collection.getEntries();
+    }
+
+    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<Void> response = 
cachedService.service(wiki).updateList(listId, csrfToken, list).execute();
+        saveLastDateHeader(response);
+    }
+
+    public void deleteList(@NonNull String csrfToken, long listId) throws 
Throwable {
+        Response<Void> response = 
cachedService.service(wiki).deleteList(listId, csrfToken).execute();
+        saveLastDateHeader(response);
+    }
+
+    public long addPageToList(@NonNull String csrfToken, long listId, 
RemoteReadingListEntry entry) throws Throwable {
+        try {
+            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();
+        } catch (Throwable t) {
+            if (isErrorType(t, "duplicate-page")) {
+                return -1;
+            }
+            throw t;
+        }
+    }
+
+    public void deletePageFromList(@NonNull String csrfToken, long listId, 
long entryId) throws Throwable {
+        Response<Void> response = 
cachedService.service(wiki).deleteEntryFromList(listId, entryId, 
csrfToken).execute();
+        saveLastDateHeader(response);
+    }
 
     public boolean isErrorType(Throwable t, @NonNull String errorType) {
         return (t instanceof HttpStatusException
@@ -94,25 +187,67 @@
                 && ((HttpStatusException) 
t).serviceError().getTitle().contains(errorType));
     }
 
+    public boolean isServiceError(Throwable t) {
+        final int code = 400;
+        return (t instanceof HttpStatusException
+                && ((HttpStatusException) t).code() == code);
+    }
+
+    private void saveLastDateHeader(Response response) {
+        lastDateHeader = response.headers().get("date");
+    }
+
     private interface Service {
 
         @POST("data/lists/setup")
-        @FormUrlEncoded
         @NonNull
-        Call<Void> setup(@Field("csrf_token") String token);
+        Call<Void> setup(@Query("csrf_token") String token);
 
         @POST("data/lists/teardown")
-        @FormUrlEncoded
         @NonNull
-        Call<Void> tearDown(@Field("csrf_token") String token);
+        Call<Void> tearDown(@Query("csrf_token") String token);
 
         @GET("data/lists/")
         @NonNull
         Call<SyncedReadingLists> getLists();
 
+        @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> getListsChangedSince(@Path("date") String 
iso8601Date);
+        Call<SyncedReadingLists> getChangesSince(@Path("date") String 
iso8601Date);
+
+        @GET("data/lists/pages/{project}/{title}")
+        @NonNull
+        Call<SyncedReadingLists> getListsContaining(@Path("project") String 
project,
+                                                    @Path("title") String 
title);
+
+        @GET("data/lists/{id}/entries/")
+        @NonNull
+        Call<SyncedReadingLists.RemoteEntryCollection> 
getListEntries(@Path("id") long listId);
+
+        @POST("data/lists/{id}/entries/")
+        @NonNull
+        Call<SyncedReadingLists.RemoteIdResponse> addEntryToList(@Path("id") 
long listId,
+                                                                 
@Query("csrf_token") String token,
+                                                                 @Body 
RemoteReadingListEntry entry);
+
+        @POST("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..8703a51
--- /dev/null
+++ 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSyncAdapter.java
@@ -0,0 +1,473 @@
+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.support.annotation.Nullable;
+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.database.http.HttpStatus;
+import org.wikipedia.dataclient.WikiSite;
+import org.wikipedia.page.PageTitle;
+import org.wikipedia.readinglist.ReadingList;
+import org.wikipedia.readinglist.page.ReadingListPage;
+import org.wikipedia.readinglist.page.ReadingListPageRow;
+import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
+import org.wikipedia.readinglist.page.database.ReadingListPageDao;
+import org.wikipedia.readinglist.page.database.ReadingListPageHttpRow;
+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.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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";
+
+    public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
+        super(context, autoInitialize);
+    }
+
+    public ReadingListSyncAdapter(Context context, boolean autoInitialize, 
boolean allowParallelSyncs) {
+        super(context, autoInitialize, allowParallelSyncs);
+    }
+
+    public static void manualSyncWithListUpdate(@NonNull ReadingList list) {
+        List<Long> ids = Prefs.getReadingListsDirtyIds();
+        if (list.remoteId() > 0 && !ids.contains(list.remoteId())) {
+            ids.add(list.remoteId());
+            Prefs.setReadingListsDirtyIds(ids);
+        }
+        manualSync();
+    }
+
+    public static void manualSyncWithListDelete(@NonNull ReadingList list) {
+        List<Long> ids = Prefs.getReadingListsDeletedIds();
+        if (list.remoteId() > 0 && !ids.contains(list.remoteId())) {
+            ids.add(list.remoteId());
+            Prefs.setReadingListsDeletedIds(ids);
+        }
+        manualSync();
+    }
+
+    public static void manualSyncWithForce() {
+        Bundle extras = new Bundle();
+        extras.putBoolean(SYNC_EXTRAS_FORCE_FULL_SYNC, true);
+        manualSync(extras);
+    }
+
+    public static void manualSync() {
+        manualSync(new Bundle());
+    }
+
+    private static void manualSync(@NonNull Bundle extras) {
+        if (AccountUtil.account() == null) {
+            return;
+        }
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
+        ContentResolver.requestSync(AccountUtil.account(), 
BuildConfig.READING_LISTS_AUTHORITY, extras);
+    }
+
+    public static void syncSavedPages() {
+        SavedPageSyncService.enqueueService(WikipediaApp.getInstance());
+    }
+
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority,
+                              ContentProviderClient provider, SyncResult 
syncResult) {
+
+        syncSavedPages();
+
+        if (!ReleaseUtil.isPreBetaRelease()  // TODO: remove when ready for 
beta/production
+                || !AccountUtil.isLoggedIn()
+                || !(Prefs.isReadingListSyncEnabled() || 
Prefs.isReadingListsRemoteDeletePending())) {
+            L.d("Skipping sync of reading lists.");
+            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 synchronizing reading lists...");
+
+        List<String> csrfToken = new ArrayList<>();
+        Map<Long, List<RemoteReadingListEntry>> remoteListMap = new 
HashMap<>();
+        List<Long> listIdsDirty = Prefs.getReadingListsDirtyIds();
+        List<Long> listIdsDeleted = Prefs.getReadingListsDeletedIds();
+
+        @NonNull List<ReadingList> allLocalLists = 
ReadingList.DAO.queryMruLists(null);
+
+        Collection<ReadingListPageHttpRow> pendingHttpRows = 
ReadingListPageDao.instance().startHttpTransaction();
+
+        WikiSite wiki = WikipediaApp.getInstance().getWikiSite();
+        ReadingListClient client = new ReadingListClient(wiki);
+
+        String lastSyncTime = Prefs.getReadingListsLastSyncTime();
+
+        try {
+
+            if (Prefs.isReadingListsRemoteDeletePending() || 
extras.containsKey(SYNC_EXTRAS_FORCE_FULL_SYNC)) {
+                // reset the remote ID on all lists, since they will need to 
be recreated next time.
+                for (ReadingList localList : allLocalLists) {
+                    localList.remoteId(-1);
+                    ReadingList.DAO.saveListInfo(localList);
+                }
+            }
+
+            // Are we scheduled for a teardown? If so, delete everything and 
bail.
+            if (Prefs.isReadingListsRemoteDeletePending()) {
+                client.tearDown(getCsrfToken(wiki, csrfToken));
+                Prefs.setReadingListsRemoteDeletePending(false);
+                return;
+            }
+
+            //-----------------------------------------------
+            // PHASE 1: Sync from remote to local.
+            //-----------------------------------------------
+
+            boolean syncAllLists = false;
+
+            List<RemoteReadingList> remoteListsModified = 
Collections.emptyList();
+            List<RemoteReadingListEntry> remoteEntriesModified = 
Collections.emptyList();
+
+            if (TextUtils.isEmpty(lastSyncTime) || 
extras.containsKey(SYNC_EXTRAS_FORCE_FULL_SYNC)) {
+                syncAllLists = true;
+            } else {
+                SyncedReadingLists allChanges = 
client.getChangesSince(lastSyncTime);
+                if (allChanges.getLists() != null) {
+                    remoteListsModified = allChanges.getLists();
+                }
+                if (allChanges.getEntries() != null) {
+                    remoteEntriesModified = allChanges.getEntries();
+                }
+            }
+
+            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. 
Falling back to full sync.");
+                    syncAllLists = true;
+                } else {
+                    if (remoteEntry.isDeleted()) {
+                        ReadingListPage pageToDelete = null;
+                        for (ReadingListPage page : eigenList.getPages()) {
+                            PageTitle remoteTitle = 
pageTitleFromRemoteEntry(remoteEntry);
+                            PageTitle localTitle = 
ReadingListDaoProxy.pageTitle(page);
+                            if (localTitle.equals(remoteTitle)) {
+                                pageToDelete = page;
+                            }
+                        }
+                        if (pageToDelete != null) {
+                            deletePageFromList(eigenList, pageToDelete);
+                        }
+                    } else {
+                        createOrUpdatePage(allLocalLists, eigenList, 
remoteEntry);
+                    }
+                }
+            }
+
+            if (syncAllLists) {
+                remoteListsModified = client.getAllLists();
+            }
+
+            for (RemoteReadingList remoteList : remoteListsModified) {
+                //if (remoteList.isDefault()) {
+                //    continue;
+                //}
+
+                // Find the remote list in our local lists...
+                ReadingList localList = null;
+                boolean upsertNeeded = false;
+
+                for (ReadingList list : allLocalLists) {
+                    if (list.remoteId() == remoteList.id()) {
+                        localList = list;
+                        break;
+                    } else if (list.getTitle().equals(remoteList.name())) {
+                        list.remoteId(remoteList.id());
+                        upsertNeeded = true;
+                        localList = list;
+                    }
+                }
+
+                if (remoteList.isDeleted()) {
+                    if (localList != null) {
+                        while (localList.getPages().size() > 0) {
+                            ReadingListPage page = localList.getPages().get(0);
+                            deletePageFromList(localList, page);
+                        }
+                        ReadingList.DAO.removeList(localList);
+                        allLocalLists.remove(localList);
+                    }
+                    continue;
+                }
+
+                List<RemoteReadingListEntry> remoteEntries = 
getRemoteListContents(client, remoteListMap, remoteList.id());
+
+                if (localList == null) {
+                    // A new list needs to be created locally.
+                    long now = System.currentTimeMillis();
+                    localList = ReadingList.builder()
+                            .remoteId(remoteList.id())
+                            
.key(ReadingListDaoProxy.listKey(remoteList.name()))
+                            .title(remoteList.name())
+                            .mtime(now)
+                            .atime(now)
+                            .description(remoteList.description())
+                            .pages(new ArrayList<>())
+                            .build();
+                    ReadingList.DAO.addList(localList);
+                    allLocalLists.add(localList);
+                } else {
+                    if (!localList.getTitle().equals(remoteList.name())) {
+                        localList.setTitle(remoteList.name());
+                        upsertNeeded = true;
+                    }
+                    if 
(!localList.getDescription().equals(remoteList.description())) {
+                        localList.setDescription(remoteList.description());
+                        upsertNeeded = true;
+                    }
+                }
+                if (upsertNeeded) {
+                    ReadingList.DAO.saveListInfo(localList);
+                }
+
+                for (RemoteReadingListEntry remoteEntry : remoteEntries) {
+                    createOrUpdatePage(allLocalLists, localList, remoteEntry);
+                }
+            }
+
+            //-----------------------------------------------
+            // PHASE 2: Sync from local to remote.
+            //-----------------------------------------------
+
+            // Do any remote lists need to be deleted?
+            List<Long> idsToDelete = new ArrayList<>();
+            idsToDelete.addAll(listIdsDeleted);
+            for (Long id : idsToDelete) {
+                try {
+                    client.deleteList(getCsrfToken(wiki, csrfToken), id);
+
+                } catch (Throwable t) {
+                    if (client.isServiceError(t)) {
+                        listIdsDeleted.remove(id);
+                    }
+                    throw t;
+                }
+            }
+
+            // Determine whether any remote lists need to be created or updated
+            for (ReadingList localList : allLocalLists) {
+                RemoteReadingList remoteList =
+                        new RemoteReadingList(localList.getTitle(), 
localList.getDescription());
+
+                if (localList.remoteId() > 0) {
+                    if (listIdsDirty.contains(localList.remoteId())) {
+                        // Update remote metadata for this list.
+                        client.updateList(getCsrfToken(wiki, csrfToken), 
localList.remoteId(), remoteList);
+                        listIdsDirty.remove(localList.remoteId());
+                    }
+
+                } else {
+                    // This list needs to be created remotely.
+                    long id = client.createList(getCsrfToken(wiki, csrfToken), 
remoteList);
+                    localList.remoteId(id);
+                    ReadingList.DAO.saveListInfo(localList);
+                }
+            }
+
+            List<ReadingListPageHttpRow> rowsToUpload = new ArrayList<>();
+            rowsToUpload.addAll(pendingHttpRows);
+
+            for (ReadingListPageHttpRow row : rowsToUpload) {
+                RemoteReadingListEntry entryForRemote = 
remoteEntryFromLocalPage(row.dat());
+
+                if (row.status() == HttpStatus.ADDED) {
+                    // find all list(s) to which this row belongs
+                    for (ReadingList localList : allLocalLists) {
+                        if (row.dat().listKeys().contains(localList.key())) {
+
+                            // create the page remotely!
+                            client.addPageToList(getCsrfToken(wiki, 
csrfToken), localList.remoteId(), entryForRemote);
+                        }
+                    }
+
+                } else if (row.status() == HttpStatus.DELETED) {
+
+                    // get the server-side lists in which this entry appears
+                    List<RemoteReadingList> remoteListsWithEntry = 
client.getListsContaining(entryForRemote);
+
+                    // find the list(s) to which this row does NOT belong
+                    for (ReadingList localList : allLocalLists) {
+                        if (!row.dat().listKeys().contains(localList.key())) {
+                            for (RemoteReadingList remoteList : 
remoteListsWithEntry) {
+                                if (remoteList.id() == localList.remoteId()) {
+
+                                    List<RemoteReadingListEntry> remoteEntries
+                                            = getRemoteListContents(client, 
remoteListMap, remoteList.id());
+
+                                    // find this page in the remote list, to 
get its id...
+                                    for (RemoteReadingListEntry entry : 
remoteEntries) {
+                                        if 
(entry.project().equals(entryForRemote.project())
+                                                && 
entry.title().equals(entryForRemote.title())) {
+
+                                            // delete the page remotely!
+                                            
client.deletePageFromList(getCsrfToken(wiki, csrfToken), localList.remoteId(), 
entry.id());
+
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                pendingHttpRows.remove(row);
+                ReadingListPageDao.instance().completeHttpTransaction(row);
+            }
+
+        } catch (Throwable t) {
+            if (client.isErrorType(t, "not-set-up")) {
+                try {
+                    client.setup(getCsrfToken(wiki, csrfToken));
+                } catch (Throwable caught) {
+                    t = caught;
+                }
+            }
+            L.w(t);
+        } finally {
+
+            lastSyncTime = getLastDateFromHeader(lastSyncTime, client);
+
+            Prefs.setReadingListsLastSyncTime(lastSyncTime);
+            Prefs.setReadingListsDirtyIds(listIdsDirty);
+            Prefs.setReadingListsDeletedIds(listIdsDeleted);
+
+            // Fail any remaining http transactions
+            for (ReadingListPageHttpRow row : pendingHttpRows) {
+                ReadingListPageDao.instance().failHttpTransaction(row);
+            }
+        }
+
+    }
+
+
+
+    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(String lastSyncTime, 
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 List<RemoteReadingListEntry> 
getRemoteListContents(ReadingListClient client,
+            Map<Long, List<RemoteReadingListEntry>> localMap, long listId) 
throws Throwable {
+        if (localMap.containsKey(listId)) {
+            return localMap.get(listId);
+        }
+        List<RemoteReadingListEntry> list = client.getListEntries(listId);
+        localMap.put(listId, list);
+        return list;
+    }
+
+    private void createOrUpdatePage(@NonNull List<ReadingList> allLists, 
@NonNull ReadingList listForPage,
+                                    @NonNull RemoteReadingListEntry 
remotePage) {
+        ReadingListPage localPage = null;
+        PageTitle remoteTitle = pageTitleFromRemoteEntry(remotePage);
+        // does this page already exist in another list?
+        for (ReadingList list : allLists) {
+            for (ReadingListPage page : list.getPages()) {
+                if (page.title().equals(remoteTitle.getDisplayText())
+                        && page.namespace().code() == 
remoteTitle.namespace().code()
+                        && 
page.wikiSite().languageCode().equals(remoteTitle.getWikiSite().languageCode()))
 {
+                    localPage = page;
+                    break;
+                }
+            }
+        }
+        boolean updateNeeded = false;
+        if (localPage == null) {
+            localPage = ReadingListDaoProxy.page(listForPage, remoteTitle);
+            updateNeeded = true;
+        }
+        if (!localPage.listKeys().contains(listForPage.key())) {
+            updateNeeded = true;
+        }
+        if (remotePage.summary() != null) {
+            if 
(!localPage.description().equals(remotePage.summary().getDescription())) {
+                
localPage.setDescription(remotePage.summary().getDescription());
+                updateNeeded = true;
+            }
+            if 
(!localPage.thumbnailUrl().equals(remotePage.summary().getThumbnailUrl())) {
+                
localPage.setThumbnailUrl(remotePage.summary().getThumbnailUrl());
+                updateNeeded = true;
+            }
+        }
+        if (updateNeeded) {
+            ReadingList.DAO.addTitleToList(listForPage, localPage, false);
+            // immediately close out the associated HTTP transaction, because 
we're done with this page.
+            ReadingListPageDao.instance().completeHttpTransaction(new 
ReadingListPageHttpRow(localPage));
+        }
+    }
+
+    private void deletePageFromList(@NonNull ReadingList list, @NonNull 
ReadingListPage page) {
+        ReadingList.DAO.removeTitleFromList(list, page);
+        ReadingListPageDao.instance().completeHttpTransaction(new 
ReadingListPageHttpRow(page));
+    }
+
+    private PageTitle pageTitleFromRemoteEntry(@NonNull RemoteReadingListEntry 
remoteEntry) {
+        WikiSite wiki = new WikiSite(remoteEntry.project());
+        return new PageTitle(remoteEntry.title(), wiki);
+    }
+
+    private RemoteReadingListEntry remoteEntryFromLocalPage(@NonNull 
ReadingListPageRow localPage) {
+        PageTitle title = new PageTitle(localPage.title(), 
localPage.wikiSite());
+        return new RemoteReadingListEntry(title.getWikiSite().authority(), 
title.getPrefixedText());
+    }
+}
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSynchronizer.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSynchronizer.java
deleted file mode 100644
index 76c95f4..0000000
--- 
a/app/src/main/java/org/wikipedia/readinglist/sync/ReadingListSynchronizer.java
+++ /dev/null
@@ -1,285 +0,0 @@
-package org.wikipedia.readinglist.sync;
-
-import android.content.ContentResolver;
-import android.os.Bundle;
-import android.os.Handler;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-
-import org.wikipedia.BuildConfig;
-import org.wikipedia.WikipediaApp;
-import org.wikipedia.auth.AccountUtil;
-import org.wikipedia.concurrency.CallbackTask;
-import org.wikipedia.dataclient.WikiSite;
-import org.wikipedia.json.GsonMarshaller;
-import org.wikipedia.json.GsonUnmarshaller;
-import org.wikipedia.onboarding.PrefsOnboardingStateMachine;
-import org.wikipedia.page.Namespace;
-import org.wikipedia.page.PageTitle;
-import org.wikipedia.readinglist.ReadingList;
-import org.wikipedia.readinglist.ReadingListData;
-import org.wikipedia.readinglist.page.ReadingListPage;
-import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
-import org.wikipedia.savedpages.SavedPageSyncService;
-import org.wikipedia.settings.Prefs;
-import org.wikipedia.useroption.UserOption;
-import org.wikipedia.useroption.dataclient.UserInfo;
-import org.wikipedia.useroption.dataclient.UserOptionDataClient;
-import org.wikipedia.useroption.dataclient.UserOptionDataClientSingleton;
-import org.wikipedia.util.ReleaseUtil;
-import org.wikipedia.util.log.L;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-import static 
org.wikipedia.readinglist.sync.RemoteReadingLists.RemoteReadingList;
-import static 
org.wikipedia.readinglist.sync.RemoteReadingLists.RemoteReadingListPage;
-import static org.wikipedia.settings.Prefs.isReadingListSyncEnabled;
-import static org.wikipedia.settings.Prefs.isReadingListsRemoteDeletePending;
-
-public class ReadingListSynchronizer {
-    private static final String READING_LISTS_SYNC_OPTION = 
"userjs-reading-lists-v1";
-    private static final ReadingListSynchronizer INSTANCE = new 
ReadingListSynchronizer();
-
-    private final Handler syncHandler = new 
Handler(WikipediaApp.getInstance().getMainLooper());
-    private final SyncRunnable syncRunnable = new SyncRunnable();
-
-    public static ReadingListSynchronizer instance() {
-        return INSTANCE;
-    }
-
-    public void bumpRevAndSync() {
-        bumpRev();
-
-        // Post the sync task with a short delay, so that possible thrashes of
-        // this method don't cause a barrage of sync requests.
-        syncHandler.removeCallbacks(syncRunnable);
-        syncHandler.postDelayed(syncRunnable, TimeUnit.SECONDS.toMillis(1));
-    }
-
-    public void sync() {
-
-
-
-        ReadingListSyncAdapter.manualSync();
-
-
-
-
-        if (!ReleaseUtil.isPreBetaRelease()  // TODO: remove when ready for 
beta/production
-                || !AccountUtil.isLoggedIn()
-                || !(isReadingListSyncEnabled() || 
isReadingListsRemoteDeletePending())) {
-            syncSavedPages();
-            L.d("Skipped sync of reading lists.");
-            return;
-        }
-        /*
-        UserOptionDataClientSingleton.instance().get(new 
UserOptionDataClient.UserInfoCallback() {
-            @Override
-            public void success(@NonNull final UserInfo info) {
-                CallbackTask.execute(new CallbackTask.Task<Void>() {
-                    @Override public Void execute() throws Throwable {
-                        syncFromRemote(info);
-                        syncSavedPages();
-                        return null;
-                    }
-                });
-            }
-        });
-        */
-    }
-
-    public void syncSavedPages() {
-        SavedPageSyncService.enqueueService(WikipediaApp.getInstance());
-    }
-
-    private synchronized void syncFromRemote(@NonNull UserInfo info) {
-        long localRev = Prefs.getReadingListSyncRev();
-        RemoteReadingLists remoteReadingLists = null;
-
-        for (UserOption option : info.userjsOptions()) {
-            if (READING_LISTS_SYNC_OPTION.equals(option.key())) {
-                remoteReadingLists = GsonUnmarshaller
-                        .unmarshal(RemoteReadingLists.class, option.val());
-            }
-        }
-
-        if (Prefs.hasReadingListsCurrentUser()
-                && 
!Prefs.isReadingListsCurrentUser(AccountUtil.getUserName())) {
-            reconcileAsRightJoin(remoteReadingLists);
-            if (remoteReadingLists != null) {
-                Prefs.setReadingListSyncRev(remoteReadingLists.rev());
-            }
-        } else if ((remoteReadingLists == null) || (remoteReadingLists.rev() < 
localRev)) {
-            if (localRev == 0) {
-                // If this is the first time we're syncing, bump the rev 
explicitly.
-                bumpRev();
-            }
-            L.d("Pushing local reading lists to server.");
-            UserOptionDataClientSingleton.instance().post(new 
UserOption(READING_LISTS_SYNC_OPTION,
-                            GsonMarshaller.marshal(makeRemoteReadingLists())), 
null);
-        } else if (localRev < remoteReadingLists.rev()) {
-            L.d("Updating local reading lists from server.");
-            reconcileAsRightJoin(remoteReadingLists);
-            Prefs.setReadingListSyncRev(remoteReadingLists.rev());
-            PrefsOnboardingStateMachine.getInstance().setReadingListTutorial();
-        } else {
-            L.d("Local and remote reading lists are in sync.");
-            if (isReadingListsRemoteDeletePending()) {
-                deleteRemoteReadingLists();
-            }
-        }
-        Prefs.setReadingListsCurrentUser(AccountUtil.getUserName());
-    }
-
-    private void deleteRemoteReadingLists() {
-        CallbackTask.execute(new CallbackTask.Task<Void>() {
-            @Override public Void execute() throws Throwable {
-                UserOptionDataClientSingleton.instance().post(new 
UserOption(READING_LISTS_SYNC_OPTION, null),
-                        new UserOptionDataClient.UserOptionPostCallback() {
-                            @Override public void success() {
-                                
Prefs.setReadingListsRemoteDeletePending(false);
-                            }
-
-                            @Override public void failure(Throwable t) {
-                            }
-                        });
-                return null;
-            }
-        });
-    }
-
-    private class SyncRunnable implements Runnable {
-        @Override
-        public void run() {
-            sync();
-        }
-    }
-
-    private void bumpRev() {
-        Prefs.setReadingListSyncRev(Prefs.getReadingListSyncRev() + 1);
-    }
-
-    private void reconcileAsRightJoin(@Nullable RemoteReadingLists 
remoteReadingLists) {
-        List<ReadingList> localLists = 
ReadingListData.instance().queryMruLists(null);
-        List<RemoteReadingList> remoteLists = remoteReadingLists == null
-                ? Collections.<RemoteReadingList>emptyList() : 
remoteReadingLists.lists();
-
-        // Remove any pages that already exist in local lists from remote 
lists.
-        // At the end of this loop, whatever is left in remoteLists will be 
added.
-        for (ReadingList localList : localLists) {
-            for (RemoteReadingList remoteList : remoteLists) {
-
-                if (!localList.getTitle().equals(remoteList.title())) {
-                    continue;
-                }
-
-                for (int localPageIndex = 0; localPageIndex < 
localList.getPages().size(); localPageIndex++) {
-                    ReadingListPage localPage = 
localList.getPages().get(localPageIndex);
-
-                    boolean deleteLocalPage = true;
-                    for (int remotePageIndex = 0; remotePageIndex < 
remoteList.pages().size(); remotePageIndex++) {
-                        RemoteReadingListPage remotePage = 
remoteList.pages().get(remotePageIndex);
-                        if (localPage.title().equals(remotePage.title())
-                                && localPage.namespace().code() == 
remotePage.namespace()
-                                && 
localPage.wikiSite().languageCode().equals(remotePage.lang())) {
-                            remoteList.pages().remove(remotePageIndex--);
-                            deleteLocalPage = false;
-                        }
-                    }
-
-                    if (deleteLocalPage) {
-                        ReadingList.DAO.removeTitleFromList(localList, 
localPage);
-                        localPageIndex--;
-                    }
-                }
-            }
-        }
-
-        // Delete local list(s) if they're not present in remote lists,
-        // and/or update list properties.
-        for (ReadingList localList : localLists) {
-            boolean deleteList = true;
-            for (RemoteReadingList remoteList : remoteLists) {
-                if (remoteList.title().equals(localList.getTitle())) {
-                    // if this list title still matches one of the remote 
lists,
-                    // then rescue it from deletion, and update its metadata.
-                    deleteList = false;
-                    localList.setDescription(remoteList.desc());
-                    ReadingList.DAO.saveListInfo(localList);
-                }
-            }
-            if (deleteList) {
-                while (localList.getPages().size() > 0) {
-                    ReadingList.DAO.removeTitleFromList(localList, 
localList.getPages().get(0));
-                }
-                ReadingList.DAO.removeList(localList);
-            }
-        }
-
-        createPagesFromRemoteLists(localLists, remoteLists);
-    }
-
-    private void createPagesFromRemoteLists(@NonNull List<ReadingList> 
localLists,
-                                            @NonNull List<RemoteReadingList> 
remoteLists) {
-        for (RemoteReadingList remoteList : remoteLists) {
-
-            ReadingList localList = null;
-            // do we need to create a new list?
-            for (ReadingList list : localLists) {
-                if (remoteList.title().equals(list.getTitle())) {
-                    localList = list;
-                    break;
-                }
-            }
-            if (localList == null) {
-                long now = System.currentTimeMillis();
-                localList = ReadingList.builder()
-                        .key(ReadingListDaoProxy.listKey(remoteList.title()))
-                        .title(remoteList.title())
-                        .mtime(now)
-                        .atime(now)
-                        .description(remoteList.desc())
-                        .pages(new ArrayList<ReadingListPage>())
-                        .build();
-                ReadingList.DAO.addList(localList);
-                localLists.add(localList);
-            }
-
-            for (RemoteReadingListPage remotePage : remoteList.pages()) {
-                createPage(localLists, localList, remotePage);
-            }
-        }
-    }
-
-    private void createPage(@NonNull List<ReadingList> allLists, @NonNull 
ReadingList listForPage,
-                            @NonNull RemoteReadingListPage remotePage) {
-        ReadingListPage localPage = null;
-        // does this page already exist in another list?
-        for (ReadingList list : allLists) {
-            for (ReadingListPage page : list.getPages()) {
-                if (page.title().equals(remotePage.title())
-                        && page.namespace().code() == remotePage.namespace()
-                        && 
page.wikiSite().languageCode().equals(remotePage.lang())) {
-                    localPage = page;
-                    break;
-                }
-            }
-        }
-        if (localPage == null) {
-            localPage = ReadingListDaoProxy.page(listForPage,
-                    new 
PageTitle(Namespace.of(remotePage.namespace()).toLegacyString(),
-                            remotePage.title(),
-                            WikiSite.forLanguageCode(remotePage.lang())));
-        }
-        ReadingList.DAO.addTitleToList(listForPage, localPage, false);
-    }
-
-    @NonNull
-    private static RemoteReadingLists makeRemoteReadingLists() {
-        List<ReadingList> lists = 
ReadingListData.instance().queryMruLists(null);
-        return new RemoteReadingLists(Prefs.getReadingListSyncRev(), lists);
-    }
-}
diff --git 
a/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java 
b/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java
index 28c0c13..4bdca9e 100644
--- a/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java
+++ b/app/src/main/java/org/wikipedia/readinglist/sync/SyncedReadingLists.java
@@ -6,24 +6,43 @@
 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.ArrayList;
 import java.util.List;
 
 public class SyncedReadingLists {
 
     @SuppressWarnings("unused,NullableProblems") @Nullable private 
List<RemoteReadingList> lists;
+    @SuppressWarnings("unused,NullableProblems") @Nullable private 
List<RemoteReadingListEntry> entries;
 
     @Nullable public List<RemoteReadingList> getLists() {
         return lists;
     }
 
-    public class RemoteReadingList {
-        @SuppressWarnings("unused") private int id;
-        @SuppressWarnings("unused,NullableProblems") @Required @NonNull 
private String name;
+    @Nullable public List<RemoteReadingListEntry> getEntries() {
+        return entries;
+    }
+
+    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, @NonNull String 
description) {
+            this.name = name;
+            this.description = description;
+        }
+
+        public long id() {
+            return id;
+        }
 
         @NonNull public String name() {
             return name;
@@ -33,33 +52,78 @@
             return StringUtils.defaultString(description);
         }
 
-        //@NonNull public List<RemoteReadingListPage> pages() {
-        //    return pages;
-        //}
+        public boolean isDefault() {
+            return isDefault;
+        }
+
+        public boolean isDeleted() {
+            return deleted;
+        }
+
+        @NonNull public String updatedDate() {
+            return updated;
+        }
     }
 
-    /*
-    public static class RemoteReadingListPage {
-        @NonNull private String lang;
-        private int namespace;
-        @NonNull private String title;
+    public class RemoteEntryCollection {
+        @SuppressWarnings("unused,NullableProblems") @Nullable private 
List<RemoteReadingListEntry> entries;
 
-        RemoteReadingListPage(@NonNull String lang, int namespace, @NonNull 
String title) {
-            this.lang = lang;
-            this.namespace = namespace;
+        @Nullable public List<RemoteReadingListEntry> getEntries() {
+            return entries;
+        }
+    }
+
+    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;
         }
 
-        @NonNull public String lang() {
-            return lang;
+        public long id() {
+            return id;
         }
 
-        public int namespace() {
-            return namespace;
+        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/settings/Prefs.java 
b/app/src/main/java/org/wikipedia/settings/Prefs.java
index 79fe9ed..c602804 100644
--- a/app/src/main/java/org/wikipedia/settings/Prefs.java
+++ b/app/src/main/java/org/wikipedia/settings/Prefs.java
@@ -619,5 +619,49 @@
         remove(R.string.preference_key_feed_cards_order);
     }
 
+    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 List<Long> getReadingListsDirtyIds() {
+        List<Long> list = new ArrayList<>();
+        if (!contains(R.string.preference_key_reading_dirty_ids)) {
+            return list;
+        }
+        //noinspection unchecked
+        List<Long> tempList = GsonUnmarshaller.unmarshal(new 
TypeToken<ArrayList<Long>>(){},
+                getString(R.string.preference_key_reading_dirty_ids, null));
+        if (tempList != null) {
+            list.addAll(tempList);
+        }
+        return list;
+    }
+
+    public static void setReadingListsDirtyIds(@NonNull List<Long> list) {
+        setString(R.string.preference_key_reading_dirty_ids, 
GsonMarshaller.marshal(list));
+    }
+
+    @NonNull public static List<Long> getReadingListsDeletedIds() {
+        List<Long> list = new ArrayList<>();
+        if (!contains(R.string.preference_key_reading_deleted_ids)) {
+            return list;
+        }
+        //noinspection unchecked
+        List<Long> tempList = GsonUnmarshaller.unmarshal(new 
TypeToken<ArrayList<Long>>(){},
+                getString(R.string.preference_key_reading_deleted_ids, null));
+        if (tempList != null) {
+            list.addAll(tempList);
+        }
+        return list;
+    }
+
+    public static void setReadingListsDeletedIds(@NonNull List<Long> list) {
+        setString(R.string.preference_key_reading_deleted_ids, 
GsonMarshaller.marshal(list));
+    }
+
     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 378cfc8..c983ac8 100644
--- a/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java
+++ b/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.java
@@ -13,7 +13,7 @@
 import org.wikipedia.R;
 import org.wikipedia.WikipediaApp;
 import org.wikipedia.activity.BaseActivity;
-import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
+import org.wikipedia.readinglist.sync.ReadingListSyncAdapter;
 import org.wikipedia.theme.Theme;
 import org.wikipedia.util.ReleaseUtil;
 import org.wikipedia.util.StringUtil;
@@ -130,15 +130,14 @@
 
     private final class SyncReadingListsListener implements 
Preference.OnPreferenceChangeListener {
         @Override public boolean onPreferenceChange(final Preference 
preference, Object newValue) {
-            final ReadingListSynchronizer synchronizer = 
ReadingListSynchronizer.instance();
             if (newValue == Boolean.TRUE) {
                 ((SwitchPreferenceCompat) preference).setChecked(true);
                 Prefs.setReadingListSyncEnabled(true);
-                synchronizer.sync();
+                ReadingListSyncAdapter.manualSync();
             } else {
                 new AlertDialog.Builder(getActivity())
                         
.setMessage(R.string.reading_lists_confirm_remote_delete)
-                        
.setPositiveButton(R.string.reading_lists_confirm_remote_delete_yes, new 
DeleteRemoteListsYesListener(preference, synchronizer))
+                        
.setPositiveButton(R.string.reading_lists_confirm_remote_delete_yes, new 
DeleteRemoteListsYesListener(preference))
                         
.setNegativeButton(R.string.reading_lists_confirm_remote_delete_no, new 
DeleteRemoteListsNoListener(preference))
                         .show();
             }
@@ -169,18 +168,16 @@
 
     private static final class DeleteRemoteListsYesListener implements 
DialogInterface.OnClickListener {
         private Preference preference;
-        private ReadingListSynchronizer synchronizer;
 
-        private DeleteRemoteListsYesListener(Preference preference, 
ReadingListSynchronizer synchronizer) {
+        private DeleteRemoteListsYesListener(Preference preference) {
             this.preference = preference;
-            this.synchronizer = synchronizer;
         }
 
         @Override public void onClick(DialogInterface dialog, int which) {
             ((SwitchPreferenceCompat) preference).setChecked(false);
             Prefs.setReadingListSyncEnabled(false);
             Prefs.setReadingListsRemoteDeletePending(true);
-            synchronizer.sync();
+            ReadingListSyncAdapter.manualSync();
         }
     }
 
diff --git 
a/app/src/main/java/org/wikipedia/useroption/database/UserOptionDao.java 
b/app/src/main/java/org/wikipedia/useroption/database/UserOptionDao.java
index b2ef5d0..d833984 100644
--- a/app/src/main/java/org/wikipedia/useroption/database/UserOptionDao.java
+++ b/app/src/main/java/org/wikipedia/useroption/database/UserOptionDao.java
@@ -67,7 +67,7 @@
     }
 
     private synchronized void markUpserted(@NonNull UserOption row) {
-        httpDao.markUpserted(new UserOptionRow(row));
+        httpDao.markModified(new UserOptionRow(row));
         upsert(row);
     }
 
diff --git a/app/src/main/res/values/preference_keys.xml 
b/app/src/main/res/values/preference_keys.xml
index 9721fc0..add60a5 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -67,4 +67,7 @@
     <string 
name="preference_key_enable_offline_library">enableOfflineLibrary</string>
     <string name="preference_key_feed_cards_order">feedCardsOrder</string>
     <string name="preference_key_feed_cards_enabled">feedCardsEnabled</string>
+    <string 
name="preference_key_reading_lists_last_sync_time">readingListsLastSyncTime</string>
+    <string 
name="preference_key_reading_dirty_ids">readingListsDirtyIds</string>
+    <string 
name="preference_key_reading_deleted_ids">readingListsDeletedIds</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/391049
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ib168adf7bb5149f2e0c82ab0ffb8ba89341ab153
Gerrit-PatchSet: 1
Gerrit-Project: apps/android/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Dbrant <dbr...@wikimedia.org>

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

Reply via email to