package org.wikipedia.readinglist.sync;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import org.wikipedia.WikipediaApp;
import org.wikipedia.concurrency.CallbackTask;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.json.GsonMarshaller;
import org.wikipedia.json.GsonUnmarshaller;
import org.wikipedia.login.User;
import org.wikipedia.page.Namespace;
import org.wikipedia.page.PageTitle;
import org.wikipedia.readinglist.ReadingList;
import org.wikipedia.readinglist.ReadingListData;
import org.wikipedia.readinglist.page.ReadingListPage;
import org.wikipedia.readinglist.page.database.ReadingListDaoProxy;
import org.wikipedia.savedpages.SavedPageSyncService;
import org.wikipedia.settings.Prefs;
import org.wikipedia.useroption.UserOption;
import org.wikipedia.useroption.dataclient.UserInfo;
import org.wikipedia.useroption.dataclient.UserOptionDataClientSingleton;
import org.wikipedia.util.ReleaseUtil;
import org.wikipedia.util.log.L;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.wikipedia.readinglist.sync.RemoteReadingLists.RemoteReadingList;
import static org.wikipedia.readinglist.sync.RemoteReadingLists.RemoteReadingListPage;
public class ReadingListSynchronizer {
private static final String READING_LISTS_SYNC_OPTION = "userjs-reading-lists-v1";
private static final ReadingListSynchronizer INSTANCE = new ReadingListSynchronizer();
private final Handler syncHandler = new Handler();
private final SyncRunnable syncRunnable = new SyncRunnable();
public static ReadingListSynchronizer instance() {
return INSTANCE;
}
public void bumpRevAndSync() {
bumpRev();
// Post the sync task with a short delay, so that possible thrashes of
// this method don't cause a barrage of sync requests.
syncHandler.removeCallbacks(syncRunnable);
syncHandler.postDelayed(syncRunnable, TimeUnit.SECONDS.toMillis(1));
}
public void sync() {
// TODO: remove when ready for beta/production
if (!ReleaseUtil.isPreBetaRelease()) {
syncSavedPages();
return;
}
if (!User.isLoggedIn()) {
L.d("Not logged in, so skipping sync of reading lists.");
syncSavedPages();
return;
}
CallbackTask.execute(new CallbackTask.Task<Void>() {
@Override
public Void execute() {
try {
UserInfo info = UserOptionDataClientSingleton.instance().get();
synchronized (ReadingListSynchronizer.this) {
long localRev = Prefs.getReadingListSyncRev();
RemoteReadingLists remoteReadingLists = null;
for (UserOption option : info.userjsOptions()) {
if (READING_LISTS_SYNC_OPTION.equals(option.key())) {
remoteReadingLists = GsonUnmarshaller
.unmarshal(RemoteReadingLists.class, option.val());
}
}
if ((remoteReadingLists == null) || (remoteReadingLists.rev() < localRev)) {
if (localRev == 0) {
// If this is the first time we're syncing, bump the rev explicitly.
bumpRev();
}
L.d("Pushing local reading lists to server.");
UserOptionDataClientSingleton.instance()
.post(new UserOption(READING_LISTS_SYNC_OPTION,
GsonMarshaller.marshal(makeRemoteReadingLists())));
} else if (localRev < remoteReadingLists.rev()) {
L.d("Updating local reading lists from server.");
reconcileAsRightJoin(remoteReadingLists);
Prefs.setReadingListSyncRev(remoteReadingLists.rev());
WikipediaApp.getInstance().getOnboardingStateMachine().setReadingListTutorial();
postEvent(new ReadingListSyncEvent());
} else {
L.d("Local and remote reading lists are in sync.");
}
}
syncSavedPages();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}, null);
}
private class SyncRunnable implements Runnable {
@Override
public void run() {
sync();
}
}
private void bumpRev() {
Prefs.setReadingListSyncRev(Prefs.getReadingListSyncRev() + 1);
}
private void postEvent(@NonNull final Object event) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override public void run() {
WikipediaApp.getInstance().getBus().post(event);
}
});
}
private void reconcileAsRightJoin(@NonNull RemoteReadingLists remoteReadingLists) {
List<ReadingList> localLists = ReadingListData.instance().queryMruLists(null);
List<RemoteReadingList> remoteLists = remoteReadingLists.lists();
// Remove any pages that already exist in local lists from remote lists.
// At the end of this loop, whatever is left in remoteLists will be added.
for (ReadingList localList : localLists) {
for (RemoteReadingList remoteList : remoteLists) {
if (!localList.getTitle().equals(remoteList.title())) {
continue;
}
for (int localPageIndex = 0; localPageIndex < localList.getPages().size(); localPageIndex++) {
ReadingListPage localPage = localList.getPages().get(localPageIndex);
boolean deleteLocalPage = true;
for (int remotePageIndex = 0; remotePageIndex < remoteList.pages().size(); remotePageIndex++) {
RemoteReadingListPage remotePage = remoteList.pages().get(remotePageIndex);
if (localPage.title().equals(remotePage.title())
&& localPage.namespace().code() == remotePage.namespace()
&& localPage.wikiSite().languageCode().equals(remotePage.lang())) {
remoteList.pages().remove(remotePageIndex--);
deleteLocalPage = false;
}
}
if (deleteLocalPage) {
ReadingList.DAO.removeTitleFromList(localList, localPage);
localPageIndex--;
}
}
}
}
// Delete local list(s) if they're not present in remote lists,
// and/or update list properties.
for (ReadingList localList : localLists) {
boolean deleteList = true;
for (RemoteReadingList remoteList : remoteLists) {
if (remoteList.title().equals(localList.getTitle())) {
// if this list title still matches one of the remote lists,
// then rescue it from deletion, and update its metadata.
deleteList = false;
localList.setDescription(remoteList.desc());
ReadingList.DAO.saveListInfo(localList);
}
}
if (deleteList) {
while (localList.getPages().size() > 0) {
ReadingList.DAO.removeTitleFromList(localList, localList.getPages().get(0));
}
ReadingList.DAO.removeList(localList);
}
}
createPagesFromRemoteLists(localLists, remoteLists);
}
private void createPagesFromRemoteLists(@NonNull List<ReadingList> localLists,
@NonNull List<RemoteReadingList> remoteLists) {
for (RemoteReadingList remoteList : remoteLists) {
ReadingList localList = null;
// do we need to create a new list?
for (ReadingList list : localLists) {
if (remoteList.title().equals(list.getTitle())) {
localList = list;
break;
}
}
if (localList == null) {
long now = System.currentTimeMillis();
localList = ReadingList.builder()
.key(ReadingListDaoProxy.listKey(remoteList.title()))
.title(remoteList.title())
.mtime(now)
.atime(now)
.description(remoteList.desc())
.pages(new ArrayList<ReadingListPage>())
.build();
ReadingList.DAO.addList(localList);
localLists.add(localList);
}
for (RemoteReadingListPage remotePage : remoteList.pages()) {
createPage(localLists, localList, remotePage);
}
}
}
private void createPage(@NonNull List<ReadingList> allLists, @NonNull ReadingList listForPage,
@NonNull RemoteReadingListPage remotePage) {
ReadingListPage localPage = null;
// does this page already exist in another list?
for (ReadingList list : allLists) {
for (ReadingListPage page : list.getPages()) {
if (page.title().equals(remotePage.title())
&& page.namespace().code() == remotePage.namespace()
&& page.wikiSite().languageCode().equals(remotePage.lang())) {
localPage = page;
break;
}
}
}
if (localPage == null) {
localPage = ReadingListDaoProxy.page(listForPage,
new PageTitle(Namespace.of(remotePage.namespace()).toLegacyString(),
remotePage.title(),
WikiSite.forLanguageCode(remotePage.lang())));
}
ReadingList.DAO.addTitleToList(listForPage, localPage, false);
}
@NonNull
private static RemoteReadingLists makeRemoteReadingLists() {
List<ReadingList> lists = ReadingListData.instance().queryMruLists(null);
return new RemoteReadingLists(Prefs.getReadingListSyncRev(), lists);
}
private void syncSavedPages() {
WikipediaApp.getInstance().startService(new Intent(WikipediaApp.getInstance(), SavedPageSyncService.class));
}
}