Search in sources :

Example 1 with RemoteReadingList

use of org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingList in project apps-android-wikipedia by wikimedia.

the class ReadingListSyncAdapter method onPerformSync.

@SuppressWarnings("methodlength")
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
    if (isDisabledByRemoteConfig() || !AccountUtil.isLoggedIn() || !(Prefs.isReadingListSyncEnabled() || Prefs.shouldShowReadingListSyncMergePrompt() || Prefs.isReadingListsRemoteDeletePending())) {
        L.d("Skipping sync of reading lists.");
        if (extras.containsKey(SYNC_EXTRAS_REFRESHING)) {
            SavedPageSyncService.sendSyncEvent();
        }
        return;
    }
    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);
    ReadingListSyncNotification readingListSyncNotification = ReadingListSyncNotification.getInstance();
    String lastSyncTime = Prefs.getReadingListsLastSyncTime();
    boolean shouldSendSyncEvent = extras.containsKey(SYNC_EXTRAS_REFRESHING);
    boolean shouldRetry = false;
    boolean shouldRetryWithForce = false;
    try {
        IN_PROGRESS = true;
        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?
            if (client.setup(getCsrfToken(wiki, csrfToken))) {
                // Set up for the first time, which means that we don't need to ask whether
                // the user wants to merge local lists.
                Prefs.shouldShowReadingListSyncMergePrompt(false);
            }
            Prefs.setReadingListsRemoteSetupPending(false);
        }
        if (Prefs.shouldShowReadingListSyncMergePrompt()) {
            Prefs.shouldShowReadingListSyncMergePrompt(false);
            for (ReadingList list : allLocalLists) {
                for (ReadingListPage page : list.pages()) {
                    if (page.remoteId() <= 0) {
                        // At least one page in our collection is not synced.
                        // We therefore need to ask the user if we want to merge unsynced pages
                        // with the remote collection, or delete them.
                        // However, let's issue a request to the changes endpoint, so that
                        // it can throw an exception if lists are not set up for the user.
                        client.getChangesSince(DateUtil.getIso8601DateFormat().format(new Date()));
                        // Exception wasn't thrown, so post the bus event.
                        WikipediaApp.getInstance().getBus().post(new ReadingListsMergeLocalDialogEvent());
                        return;
                    }
                }
            }
        }
        // -----------------------------------------------
        // 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();
        }
        // Notify any event consumers that reading lists are, in fact, enabled.
        WikipediaApp.getInstance().getBus().post(new ReadingListsEnabledStatusEvent());
        // setup syncing indicator for remote to local
        int remoteItemsTotal = remoteListsModified.size();
        int remoteItemsSynced = 0;
        // First, update our list hierarchy to match the remote hierarchy.
        for (RemoteReadingList remoteList : remoteListsModified) {
            readingListSyncNotification.setNotificationProgress(getContext(), remoteItemsTotal, remoteItemsSynced++);
            // 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 (StringUtil.normalizedEquals(list.title(), remoteList.name())) {
                    localList = list;
                    localList.remoteId(remoteList.id());
                    upsertNeeded = true;
                    break;
                }
            }
            if (remoteList.isDefault() && localList != null && !localList.isDefault()) {
                L.logRemoteError(new RuntimeException("Unexpected: remote default list corresponds to local non-default list."));
                localList = ReadingListDbHelper.instance().getDefaultList();
            }
            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());
                if (remoteList.isDefault()) {
                    L.logRemoteError(new RuntimeException("Unexpected: local default list no longer matches remote."));
                    localList = ReadingListDbHelper.instance().getDefaultList();
                } else {
                    localList = ReadingListDbHelper.instance().createList(remoteList.name(), remoteList.description());
                }
                localList.remoteId(remoteList.id());
                allLocalLists.add(localList);
                upsertNeeded = true;
            } else {
                if (!localList.isDefault() && !StringUtil.normalizedEquals(localList.title(), remoteList.name())) {
                    localList.title(remoteList.name());
                    upsertNeeded = true;
                }
                if (!localList.isDefault() && !StringUtil.normalizedEquals(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);
        }
        // setup syncing indicator for local to remote
        int localItemsTotal = allLocalLists.size();
        int localItemsSynced = 0;
        // Determine whether any remote lists need to be created or updated
        for (ReadingList localList : allLocalLists) {
            readingListSyncNotification.setNotificationProgress(getContext(), localItemsTotal, localItemsSynced++);
            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) {
            List<ReadingListPage> localPages = new ArrayList<>();
            List<RemoteReadingListEntry> newEntries = new ArrayList<>();
            for (ReadingListPage localPage : localList.pages()) {
                if (localPage.remoteId() < 1) {
                    localPages.add(localPage);
                    newEntries.add(remoteEntryFromLocalPage(localPage));
                }
            }
            if (newEntries.isEmpty()) {
                continue;
            }
            try {
                if (localPages.size() == 1) {
                    L.d("Creating new remote page " + localPages.get(0).title());
                    localPages.get(0).remoteId(client.addPageToList(getCsrfToken(wiki, csrfToken), localList.remoteId(), newEntries.get(0)));
                    ReadingListDbHelper.instance().updatePage(localPages.get(0));
                } else {
                    L.d("Creating " + newEntries.size() + " new remote pages");
                    List<Long> ids = client.addPagesToList(getCsrfToken(wiki, csrfToken), localList.remoteId(), newEntries);
                    for (int i = 0; i < ids.size(); i++) {
                        localPages.get(i).remoteId(ids.get(i));
                    }
                    ReadingListDbHelper.instance().updatePages(localPages);
                }
            } catch (Throwable t) {
                // so let's force a full sync.
                if (client.isErrorType(t, "duplicate-page")) {
                    shouldRetryWithForce = true;
                    break;
                } else if (client.isErrorType(t, "entry-limit")) {
                // TODO: handle more meaningfully than ignoring, for now.
                } else {
                    throw t;
                }
            }
        }
    } catch (Throwable t) {
        if (client.isErrorType(t, "not-set-up")) {
            Prefs.setReadingListSyncEnabled(false);
            if (TextUtils.isEmpty(lastSyncTime)) {
                // This means that it's our first time attempting to sync, and we see that
                // syncing isn't enabled on the server. So, let's prompt the user to enable it:
                WikipediaApp.getInstance().getBus().post(new ReadingListsEnableDialogEvent());
            } else {
                // This can only mean that our reading lists have been torn down (disabled) by
                // another client, so we need to notify the user of this development.
                WikipediaApp.getInstance().getBus().post(new ReadingListsNoLongerSyncedEvent());
            }
        }
        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);
        readingListSyncNotification.cancelNotification(getContext());
        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);
        }
        IN_PROGRESS = false;
        SavedPageSyncService.enqueue();
        L.d("Finished sync of reading lists.");
    }
}
Also used : ReadingListsNoLongerSyncedEvent(org.wikipedia.events.ReadingListsNoLongerSyncedEvent) ReadingListsEnabledStatusEvent(org.wikipedia.events.ReadingListsEnabledStatusEvent) ArrayList(java.util.ArrayList) WikiSite(org.wikipedia.dataclient.WikiSite) RemoteReadingList(org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingList) ReadingListsEnableDialogEvent(org.wikipedia.events.ReadingListsEnableDialogEvent) ReadingListPage(org.wikipedia.readinglist.database.ReadingListPage) RemoteReadingListEntry(org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingListEntry) ReadingListsMergeLocalDialogEvent(org.wikipedia.events.ReadingListsMergeLocalDialogEvent) Bundle(android.os.Bundle) Date(java.util.Date) ReadingList(org.wikipedia.readinglist.database.ReadingList) RemoteReadingList(org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingList)

Aggregations

Bundle (android.os.Bundle)1 ArrayList (java.util.ArrayList)1 Date (java.util.Date)1 WikiSite (org.wikipedia.dataclient.WikiSite)1 ReadingListsEnableDialogEvent (org.wikipedia.events.ReadingListsEnableDialogEvent)1 ReadingListsEnabledStatusEvent (org.wikipedia.events.ReadingListsEnabledStatusEvent)1 ReadingListsMergeLocalDialogEvent (org.wikipedia.events.ReadingListsMergeLocalDialogEvent)1 ReadingListsNoLongerSyncedEvent (org.wikipedia.events.ReadingListsNoLongerSyncedEvent)1 ReadingList (org.wikipedia.readinglist.database.ReadingList)1 ReadingListPage (org.wikipedia.readinglist.database.ReadingListPage)1 RemoteReadingList (org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingList)1 RemoteReadingListEntry (org.wikipedia.readinglist.sync.SyncedReadingLists.RemoteReadingListEntry)1