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