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