package com.florianmski.tracktoid.services;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.support.v4.app.NotificationCompat;
import com.florianmski.tracktoid.utils.CVHelper;
import com.florianmski.tracktoid.utils.DateHelper;
import com.florianmski.tracktoid.utils.DbHelper;
import com.florianmski.tracktoid.R;
import com.florianmski.tracktoid.TraktoidPrefs;
import com.florianmski.tracktoid.TraktoidTheme;
import com.florianmski.tracktoid.data.database.ProviderSchematic;
import com.florianmski.tracktoid.data.database.columns.SyncColumns;
import com.florianmski.tracktoid.data.database.utils.CVUtils;
import com.florianmski.tracktoid.errors.ErrorHandler;
import com.florianmski.tracktoid.errors.RetrofitComportment;
import com.florianmski.tracktoid.rx.observables.TraktObservable;
import com.florianmski.tracktoid.trakt.TraktManager;
import com.uwetrottmann.trakt.v2.entities.BaseEpisode;
import com.uwetrottmann.trakt.v2.entities.BaseMovie;
import com.uwetrottmann.trakt.v2.entities.BaseRatedEntity;
import com.uwetrottmann.trakt.v2.entities.BaseSeason;
import com.uwetrottmann.trakt.v2.entities.BaseShow;
import com.uwetrottmann.trakt.v2.entities.Episode;
import com.uwetrottmann.trakt.v2.entities.LastActivities;
import com.uwetrottmann.trakt.v2.entities.LastActivity;
import com.uwetrottmann.trakt.v2.entities.LastActivityMore;
import com.uwetrottmann.trakt.v2.entities.Movie;
import com.uwetrottmann.trakt.v2.entities.RatedEpisode;
import com.uwetrottmann.trakt.v2.entities.RatedMovie;
import com.uwetrottmann.trakt.v2.entities.RatedSeason;
import com.uwetrottmann.trakt.v2.entities.RatedShow;
import com.uwetrottmann.trakt.v2.entities.Season;
import com.uwetrottmann.trakt.v2.entities.Show;
import com.uwetrottmann.trakt.v2.entities.WatchlistedEpisode;
import com.uwetrottmann.trakt.v2.entities.WatchlistedSeason;
import com.uwetrottmann.trakt.v2.enums.Extended;
import com.uwetrottmann.trakt.v2.enums.Rating;
import com.uwetrottmann.trakt.v2.enums.RatingsFilter;
import com.uwetrottmann.trakt.v2.exceptions.OAuthUnauthorizedException;
import org.joda.time.DateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import retrofit.RetrofitError;
import rx.Observable;
import rx.Observer;
import rx.functions.Action0;
import rx.functions.Action2;
import rx.functions.Func1;
import rx.functions.Func2;
import rx.functions.Func3;
import rx.functions.Func4;
import timber.log.Timber;
public class TraktoidSynchronizer
{
public final static int NOTIFICATION_ID = 42;
private Context context;
private DateTime startSyncTime;
private DateTime lastLocalSyncTime;
private ErrorHandler errorHandler;
private NotificationManager notificationManager;
private NotificationCompat.Builder notificationBuilder;
public TraktoidSynchronizer(Context context)
{
this.context = context;
errorHandler = new ErrorHandler(context)
.putComportment(new RetrofitComportment())
.reportToUser(false);
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
cancelNotification();
notificationBuilder = new NotificationCompat.Builder(context)
.setContentTitle("Trakt sync in progress...")
.setColor(TraktoidTheme.DEFAULT.getColorDark(context))
.setSmallIcon(R.drawable.ic_sync_white_24dp)
.setOngoing(true)
.setProgress(0, 0, true);
updateNotification();
}
// have to do this because we do not have the trakt id for an episode in /sync/watched/shows and collected
private static class EpisodeKey
{
private final static String SEPARATOR = ",";
public String traktShow;
public int season, number;
public String key;
public EpisodeKey(String key)
{
this.key = key;
String[] data = key.split(SEPARATOR);
this.traktShow = data[0];
this.season = Integer.valueOf(data[1]);
this.number = Integer.valueOf(data[2]);
}
public static String get(String traktShow, int season, int number)
{
return String.format("%s%s%d%s%d", traktShow, SEPARATOR, season, SEPARATOR, number);
}
}
private <T> CVHelper getCVHelper(T key, Map<T, ContentValues> map)
{
ContentValues contentValues = map.get(key);
if(contentValues == null)
contentValues = new ContentValues();
return new CVHelper(contentValues);
}
private static class BaseSync
{
public DateTime last_collected_at;
public DateTime listed_at;
public DateTime rated_at;
public DateTime last_watched_at;
public Integer plays;
public Rating rating;
public String key;
protected BaseSync() {}
public BaseSync (BaseRatedEntity baseRatedEntity)
{
this.rated_at = baseRatedEntity.rated_at;
this.rating = baseRatedEntity.rating;
}
protected ContentValues pack()
{
return new ContentValues();
}
}
private static class ShowSync extends BaseSync
{
public ShowSync(RatedShow ratedShow)
{
super(ratedShow);
setKey(ratedShow.show);
}
public ShowSync(BaseShow baseShow)
{
setKey(baseShow.show);
this.last_collected_at = baseShow.last_collected_at;
this.last_watched_at = baseShow.last_watched_at;
this.listed_at = baseShow.listed_at;
this.plays = baseShow.plays;
}
private void setKey(Show show)
{
this.key = String.valueOf(show.ids.trakt);
}
protected static List<ShowSync> toListFromRated(List<RatedShow> ratedShows)
{
List<ShowSync> list = new ArrayList<>();
for(RatedShow ratedShow : ratedShows)
list.add(new ShowSync(ratedShow));
return list;
}
protected static List<ShowSync> toListFromBase(List<BaseShow> baseShows)
{
List<ShowSync> list = new ArrayList<>();
for(BaseShow baseShow : baseShows)
list.add(new ShowSync(baseShow));
return list;
}
}
private static class SeasonSync extends BaseSync
{
public SeasonSync(RatedSeason ratedSeason)
{
super(ratedSeason);
setKey(ratedSeason.season);
}
public SeasonSync(WatchlistedSeason watchlistedSeason)
{
setKey(watchlistedSeason.season);
this.last_collected_at = watchlistedSeason.listed_at;
}
private void setKey(Season season)
{
this.key = String.valueOf(season.ids.trakt);
}
protected static List<SeasonSync> toListFromRated(List<RatedSeason> ratedSeasons)
{
List<SeasonSync> list = new ArrayList<>();
for(RatedSeason ratedSeason : ratedSeasons)
list.add(new SeasonSync(ratedSeason));
return list;
}
protected static List<SeasonSync> toListFromWatchlisted(List<WatchlistedSeason> watchlistedSeasons)
{
List<SeasonSync> list = new ArrayList<>();
for(WatchlistedSeason watchlistedSeason : watchlistedSeasons)
list.add(new SeasonSync(watchlistedSeason));
return list;
}
}
private static class EpisodeSync extends BaseSync
{
public EpisodeSync(RatedEpisode ratedEpisode)
{
super(ratedEpisode);
setKey(String.valueOf(ratedEpisode.show.ids.trakt), ratedEpisode.episode);
}
public EpisodeSync(WatchlistedEpisode watchlistedEpisode)
{
setKey(String.valueOf(watchlistedEpisode.show.ids.trakt), watchlistedEpisode.episode);
this.listed_at = watchlistedEpisode.listed_at;
}
public EpisodeSync(String traktShow, int season, BaseEpisode baseEpisode)
{
setKey(traktShow, season, baseEpisode.number);
this.last_collected_at = baseEpisode.collected_at;
this.plays = baseEpisode.plays;
}
private void setKey(String traktShow, int season, int number)
{
this.key = EpisodeKey.get(traktShow, season, number);
}
private void setKey(String traktShow, Episode episode)
{
setKey(traktShow, episode.season, episode.number);
}
protected static List<EpisodeSync> toListFromRated(List<RatedEpisode> ratedEpisodes)
{
List<EpisodeSync> list = new ArrayList<>();
for(RatedEpisode ratedEpisode : ratedEpisodes)
list.add(new EpisodeSync(ratedEpisode));
return list;
}
protected static List<EpisodeSync> toListFromBase(BaseShow baseShow, int season, List<BaseEpisode> baseEpisodes)
{
List<EpisodeSync> list = new ArrayList<>();
for(BaseEpisode baseEpisode : baseEpisodes)
{
EpisodeSync episodeSync = new EpisodeSync(String.valueOf(baseShow.show.ids.trakt), season, baseEpisode);
// transmit the last_watched_at to episodes because they don't have one, bug in the API?
episodeSync.last_watched_at = baseShow.last_watched_at;
list.add(episodeSync);
}
return list;
}
protected static List<EpisodeSync> toListFromShows(List<BaseShow> baseShows)
{
List<EpisodeSync> list = new ArrayList<>();
for(BaseShow baseShow : baseShows)
for(BaseSeason baseSeason : baseShow.seasons)
list.addAll(toListFromBase(baseShow, baseSeason.number, baseSeason.episodes));
return list;
}
protected static List<EpisodeSync> toListFromWatchlisted(List<WatchlistedEpisode> watchlistedEpisodes)
{
List<EpisodeSync> list = new ArrayList<>();
for(WatchlistedEpisode watchlistedEpisode : watchlistedEpisodes)
list.add(new EpisodeSync(watchlistedEpisode));
return list;
}
}
private static class MovieSync extends BaseSync
{
public Movie movie;
public MovieSync(Movie movie)
{
this.movie = movie;
setKey(movie);
}
public MovieSync(RatedMovie ratedMovie)
{
super(ratedMovie);
this.movie = ratedMovie.movie;
setKey(ratedMovie.movie);
}
public MovieSync(BaseMovie baseMovie)
{
this(baseMovie.movie);
this.last_collected_at = baseMovie.collected_at;
this.last_watched_at = baseMovie.last_watched_at;
this.listed_at = baseMovie.listed_at;
this.plays = baseMovie.plays;
}
private void setKey(Movie movie)
{
this.key = String.valueOf(movie.ids.trakt);
}
@Override
protected ContentValues pack()
{
return CVUtils.packMovie(movie);
}
protected static List<MovieSync> toListFromRated(List<RatedMovie> ratedMovies)
{
List<MovieSync> list = new ArrayList<>();
for(RatedMovie ratedMovie : ratedMovies)
list.add(new MovieSync(ratedMovie));
return list;
}
protected static List<MovieSync> toListFromBase(List<BaseMovie> baseMovies)
{
List<MovieSync> list = new ArrayList<>();
for(BaseMovie baseMovie : baseMovies)
list.add(new MovieSync(baseMovie));
return list;
}
}
private Func2<Integer, Throwable, Boolean> retryFunc = new Func2<Integer, Throwable, Boolean>()
{
@Override
public Boolean call(Integer integer, Throwable throwable)
{
// retry 3 times if network or server issue
boolean retry =
integer <= 3
&& throwable instanceof RetrofitError
&& (((RetrofitError) throwable).getKind() == RetrofitError.Kind.NETWORK
|| ((RetrofitError) throwable).getKind() == RetrofitError.Kind.HTTP);
Timber.d("retry n°%d, continue? %b", integer, retry);
return retry;
}
};
private boolean isSyncNeeded(DateTime lastServerSyncTime)
{
return lastServerSyncTime.isAfter(lastLocalSyncTime);
}
private <T extends BaseSync> Observable<Map<String, ContentValues>> getSyncObservable(DateTime lastServerSyncTime, TraktObservable<List<T>> traktObservable, final Func1<T, ContentValues> funcToContentValues, final Func1<T, DateTime> funcGetItemSyncTime)
{
// we don't need to run this because there's nothing new on the server
// just return an empty map
if(!isSyncNeeded(lastServerSyncTime))
return Observable.just(Collections.<String, ContentValues>emptyMap());
return Observable
.create(traktObservable)
.retry(retryFunc)
.flatMap(new Func1<List<T>, Observable<T>>()
{
@Override
public Observable<T> call(List<T> ts)
{
return Observable.from(ts);
}
})
// sort the items from the most recent to the oldest
.toSortedList(new Func2<T, T, Integer>()
{
@Override
public Integer call(T t1, T t2)
{
DateTime syncTime1 = funcGetItemSyncTime.call(t1);
DateTime syncTime2 = funcGetItemSyncTime.call(t2);
// apparently sometimes API send back null values
// in this case take "now" time
if(syncTime1 == null)
syncTime1 = DateHelper.now();
if(syncTime2 == null)
syncTime2 = DateHelper.now();
return -syncTime1.compareTo(syncTime2);
}
})
.flatMap(new Func1<List<T>, Observable<T>>()
{
@Override
public Observable<T> call(List<T> ts)
{
return Observable.from(ts);
}
})
// take the items that haven't been sync
.takeWhile(new Func1<T, Boolean>()
{
@Override
public Boolean call(T t)
{
DateTime syncTime = funcGetItemSyncTime.call(t);
if(syncTime == null)
syncTime = DateHelper.now();
return syncTime.isAfter(lastLocalSyncTime);
}
})
.collect(new ArrayList<T>(), new Action2<ArrayList<T>, T>()
{
@Override
public void call(ArrayList<T> ts, T t)
{
ts.add(t);
}
})
.map(new Func1<List<T>, Map<String, ContentValues>>()
{
@Override
public Map<String, ContentValues> call(List<T> entities)
{
return constructMap(entities, funcToContentValues);
}
});
}
private <T extends BaseSync> Map<String, ContentValues> constructMap(List<T> entities, Func1<T, ContentValues> func)
{
Map<String, ContentValues> map = new HashMap<>();
for (T entity : entities)
{
String key = entity.key;
CVHelper cvHelper = getCVHelper(key, map)
.putAll(func.call(entity))
.putAll(entity.pack());
map.put(key, cvHelper.get());
}
return map;
}
private <T extends BaseSync> Observable<Map<String, ContentValues>> getRatingsObservable(DateTime lastServerSyncTime, TraktObservable<List<T>> traktObservable)
{
return getSyncObservable(lastServerSyncTime, traktObservable, new Func1<T, ContentValues>()
{
@Override
public ContentValues call(T ratedEntity)
{
return new CVHelper()
.put(SyncColumns.RATING, ratedEntity.rating.toString())
.put(SyncColumns.RATED_AT, ratedEntity.rated_at)
.get();
}
}, new Func1<T, DateTime>()
{
@Override
public DateTime call(T ratedEntity)
{
return ratedEntity.rated_at;
}
});
}
private Observable<Map<String, ContentValues>> getRatingsShowObservable(DateTime lastServerSyncTime)
{
return getRatingsObservable(lastServerSyncTime, new TraktObservable<List<ShowSync>>()
{
@Override
public List<ShowSync> fire() throws OAuthUnauthorizedException
{
return ShowSync.toListFromRated(TraktManager.getInstance().sync().ratingsShows(RatingsFilter.ALL, Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getRatingsSeasonObservable(DateTime lastServerSyncTime)
{
return getRatingsObservable(lastServerSyncTime, new TraktObservable<List<SeasonSync>>()
{
@Override
public List<SeasonSync> fire() throws OAuthUnauthorizedException
{
return SeasonSync.toListFromRated(TraktManager.getInstance().sync().ratingsSeasons(RatingsFilter.ALL, Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getRatingsEpisodeObservable(DateTime lastServerSyncTime)
{
return getRatingsObservable(lastServerSyncTime, new TraktObservable<List<EpisodeSync>>()
{
@Override
public List<EpisodeSync> fire() throws OAuthUnauthorizedException
{
return EpisodeSync.toListFromRated(TraktManager.getInstance().sync().ratingsEpisodes(RatingsFilter.ALL, Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getRatingsMovieObservable(DateTime lastServerSyncTime)
{
return getRatingsObservable(lastServerSyncTime, new TraktObservable<List<MovieSync>>()
{
@Override
public List<MovieSync> fire() throws OAuthUnauthorizedException
{
return MovieSync.toListFromRated(TraktManager.getInstance().sync().ratingsMovies(RatingsFilter.ALL, Extended.FULLIMAGES));
}
});
}
private <T extends BaseSync> Observable<Map<String, ContentValues>> getWatchlistedObservable(DateTime lastServerSyncTime, TraktObservable<List<T>> traktObservable)
{
return getSyncObservable(lastServerSyncTime, traktObservable, new Func1<T, ContentValues>()
{
@Override
public ContentValues call(T watchlistedEntity)
{
return new CVHelper()
.put(SyncColumns.WATCHLISTED, true)
.put(SyncColumns.WATCHLISTED_AT, watchlistedEntity.listed_at)
.get();
}
}, new Func1<T, DateTime>()
{
@Override
public DateTime call(T ratedEntity)
{
return ratedEntity.listed_at;
}
});
}
private Observable<Map<String, ContentValues>> getWatchlistedShowObservable(DateTime lastServerSyncTime)
{
return getWatchlistedObservable(lastServerSyncTime, new TraktObservable<List<ShowSync>>()
{
@Override
public List<ShowSync> fire() throws OAuthUnauthorizedException
{
return ShowSync.toListFromBase(TraktManager.getInstance().sync().watchlistShows(Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getWatchlistedSeasonObservable(DateTime lastServerSyncTime)
{
return getWatchlistedObservable(lastServerSyncTime, new TraktObservable<List<SeasonSync>>()
{
@Override
public List<SeasonSync> fire() throws OAuthUnauthorizedException
{
return SeasonSync.toListFromWatchlisted(TraktManager.getInstance().sync().watchlistSeasons(Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getWatchlistedEpisodeObservable(DateTime lastServerSyncTime)
{
return getWatchlistedObservable(lastServerSyncTime, new TraktObservable<List<EpisodeSync>>()
{
@Override
public List<EpisodeSync> fire() throws OAuthUnauthorizedException
{
return EpisodeSync.toListFromWatchlisted(TraktManager.getInstance().sync().watchlistEpisodes(Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getWatchlistedMovieObservable(DateTime lastServerSyncTime)
{
return getWatchlistedObservable(lastServerSyncTime, new TraktObservable<List<MovieSync>>()
{
@Override
public List<MovieSync> fire() throws OAuthUnauthorizedException
{
return MovieSync.toListFromBase(TraktManager.getInstance().sync().watchlistMovies(Extended.FULLIMAGES));
}
});
}
private <T extends BaseSync> Observable<Map<String, ContentValues>> getWatchedObservable(DateTime lastServerSyncTime, TraktObservable<List<T>> traktObservable)
{
return getSyncObservable(lastServerSyncTime, traktObservable, new Func1<T, ContentValues>()
{
@Override
public ContentValues call(T watchedEntity)
{
return new CVHelper()
.put(SyncColumns.WATCHED, true)
.put(SyncColumns.PLAYS, watchedEntity.plays)
.put(SyncColumns.LAST_WATCHED_AT, watchedEntity.last_watched_at)
.get();
}
}, new Func1<T, DateTime>()
{
@Override
public DateTime call(T watchedEntity)
{
return watchedEntity.last_watched_at;
}
});
}
private Observable<Map<String, ContentValues>> getWatchedEpisodeObservable(DateTime lastServerSyncTime)
{
return getWatchedObservable(lastServerSyncTime, new TraktObservable<List<EpisodeSync>>()
{
@Override
public List<EpisodeSync> fire() throws OAuthUnauthorizedException
{
return EpisodeSync.toListFromShows(TraktManager.getInstance().sync().watchedShows(Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getWatchedMovieObservable(DateTime lastServerSyncTime)
{
return getWatchedObservable(lastServerSyncTime, new TraktObservable<List<MovieSync>>()
{
@Override
public List<MovieSync> fire() throws OAuthUnauthorizedException
{
return MovieSync.toListFromBase(TraktManager.getInstance().sync().watchedMovies(Extended.FULLIMAGES));
}
});
}
private <T extends BaseSync> Observable<Map<String, ContentValues>> getCollectedObservable(DateTime lastServerSyncTime, TraktObservable<List<T>> traktObservable)
{
return getSyncObservable(lastServerSyncTime, traktObservable, new Func1<T, ContentValues>()
{
@Override
public ContentValues call(T watchedEntity)
{
return new CVHelper()
.put(SyncColumns.COLLECTED, true)
.put(SyncColumns.COLLECTED_AT, watchedEntity.last_collected_at)
.get();
}
}, new Func1<T, DateTime>()
{
@Override
public DateTime call(T ratedEntity)
{
return ratedEntity.last_collected_at;
}
});
}
private Observable<Map<String, ContentValues>> getCollectedEpisodeObservable(DateTime lastServerSyncTime)
{
return getCollectedObservable(lastServerSyncTime, new TraktObservable<List<EpisodeSync>>()
{
@Override
public List<EpisodeSync> fire() throws OAuthUnauthorizedException
{
return EpisodeSync.toListFromShows(TraktManager.getInstance().sync().collectionShows(Extended.DEFAULT_MIN));
}
});
}
private Observable<Map<String, ContentValues>> getCollectedMovieObservable(DateTime lastServerSyncTime)
{
return getCollectedObservable(lastServerSyncTime, new TraktObservable<List<MovieSync>>()
{
@Override
public List<MovieSync> fire() throws OAuthUnauthorizedException
{
return MovieSync.toListFromBase(TraktManager.getInstance().sync().collectionMovies(Extended.FULLIMAGES));
}
});
}
private Observable<Map.Entry<String, ContentValues>> getMovieObservable(LastActivityMore moviesActivity)
{
return Observable.zip(
getCollectedMovieObservable(moviesActivity.collected_at),
getRatingsMovieObservable(moviesActivity.rated_at),
getWatchedMovieObservable(moviesActivity.watched_at),
getWatchlistedMovieObservable(moviesActivity.watchlisted_at),
new Func4<Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>>()
{
@Override
public Map<String, ContentValues> call(Map<String, ContentValues> mapCollected, Map<String, ContentValues> mapRatings, Map<String, ContentValues> mapWatched, Map<String, ContentValues> mapWatchlisted)
{
return mergeMaps(mapWatched, mapCollected, mapRatings, mapWatchlisted);
}
})
.flatMap(new Func1<Map<String, ContentValues>, Observable<Map.Entry<String, ContentValues>>>()
{
@Override
public Observable<Map.Entry<String, ContentValues>> call(Map<String, ContentValues> map)
{
Map<String, Long> localMovieMap = DbHelper.getLastUpdatedMovieMap(context);
List<ContentValues> moviesToInsert = new ArrayList<>();
for (Map.Entry<String, ContentValues> movieEntry : map.entrySet())
{
String traktMovie = movieEntry.getKey();
ContentValues contentValues = movieEntry.getValue();
// if we don't have it locally it means we have to insert it
if (!localMovieMap.containsKey(traktMovie))
moviesToInsert.add(contentValues);
// if we have it, update db
else
DbHelper.updateMovie(context, contentValues, traktMovie);
}
// bulkInsert movies
// bulkInsert will notify the movie CONTENT_URI
DbHelper.bulkInsert(context, moviesToInsert, ProviderSchematic.Movies.CONTENT_URI);
return Observable.from(map.entrySet());
}
});
}
private Observable<Void> getShowObservable(LastActivities lastActivities)
{
return Observable.zip(
getMergedShowObservable(lastActivities.shows),
getMergedSeasonObservable(lastActivities.seasons),
getMergedEpisodeObservable(lastActivities.episodes),
new Func3<Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>, Void>()
{
@Override
public Void call(Map<String, ContentValues> mapShows, Map<String, ContentValues> mapSeasons, Map<String, ContentValues> mapEpisodes)
{
Map<String, Long> localShowMap = DbHelper.getLastUpdatedShowMap(context);
// check if we have episodes that belong to a show we don't have already scheduled for dl
for(Map.Entry<String, ContentValues> episodeEntry : mapEpisodes.entrySet())
{
String showTrakt = new EpisodeKey(episodeEntry.getKey()).traktShow;
if (!localShowMap.containsKey(showTrakt) && !mapShows.containsKey(showTrakt))
mapShows.put(showTrakt, new ContentValues());
}
Iterator<Map.Entry<String, ContentValues>> entries = mapShows.entrySet().iterator();
while (entries.hasNext())
{
Map.Entry<String, ContentValues> entry = entries.next();
String traktShow = entry.getKey();
ContentValues contentValues = entry.getValue();
if (!localShowMap.containsKey(traktShow))
{
List<ContentValues> seasonToInsert = new ArrayList<>();
List<ContentValues> episodeToInsert = new ArrayList<>();
// download show, seasons and episodes
// add the contentvalues to the respective showToInsert, seasonToInsert and episodeToInsert
// check in the user data maps if we have additional sync infos for these items
// if so add them to our contentvalues and remove them from the map
Show show = TraktManager.getInstance().shows().summary(traktShow, Extended.FULLIMAGES);
List<Season> seasons = TraktManager.getInstance().seasons().summary(traktShow, Extended.FULLIMAGES);
for (Season season : seasons)
{
String traktSeason = String.valueOf(season.ids.trakt);
List<Episode> episodes = TraktManager.getInstance().seasons().season(traktShow, season.number, Extended.FULLIMAGES);
for (Episode episode : episodes)
{
String episodeKey = EpisodeKey.get(traktShow, episode.season, episode.number);
// add episode contentvalues and add user data if we have some
ContentValues episodeContentValues = CVUtils.packEpisode(episode, traktSeason, traktShow);
ContentValues episodeUserDataContentValues = mapEpisodes.get(episodeKey);
if (episodeUserDataContentValues != null)
{
episodeContentValues.putAll(episodeUserDataContentValues);
mapEpisodes.remove(episodeKey);
}
episodeToInsert.add(episodeContentValues);
}
// add season contentvalues and add user data if we have some
ContentValues seasonContentValues = CVUtils.packSeason(season, traktShow);
ContentValues seasonUserDataContentValues = mapSeasons.get(traktSeason);
if (seasonUserDataContentValues != null)
{
seasonContentValues.putAll(seasonUserDataContentValues);
mapSeasons.remove(traktSeason);
}
seasonToInsert.add(seasonContentValues);
}
// add show contentvalues and add user data
ContentValues showContentValues = CVUtils.packShow(show);
showContentValues.putAll(contentValues);
entries.remove();
// insert show and bulkInsert seasons, episodes
DbHelper.bulkInsert(context, episodeToInsert, ProviderSchematic.Episodes.CONTENT_URI);
DbHelper.bulkInsert(context, seasonToInsert, ProviderSchematic.Seasons.CONTENT_URI);
DbHelper.insert(context, ProviderSchematic.Shows.CONTENT_URI, showContentValues);
}
// if we have it, update db
else
DbHelper.updateShow(context, contentValues, traktShow);
}
// we still need to update what's left (seasons and episodes)
for (Map.Entry<String, ContentValues> seasonEntry : mapSeasons.entrySet())
DbHelper.updateSeason(context, seasonEntry.getValue(), seasonEntry.getKey());
for (Map.Entry<String, ContentValues> episodeEntry : mapEpisodes.entrySet())
{
EpisodeKey episodeKey = new EpisodeKey(episodeEntry.getKey());
// have to update like this because we don't have the traktId of the episode in the
// sync/collection/shows and sync/watched/shows methods
DbHelper.updateEpisode(context, episodeEntry.getValue(), episodeKey.traktShow, episodeKey.season, episodeKey.number);
}
// TODO what are we suppose to return ?
return null;
}
});
}
private Observable<Map<String, ContentValues>> getMergedShowObservable(LastActivity showActivities)
{
return Observable.zip(
getRatingsShowObservable(showActivities.rated_at),
getWatchlistedShowObservable(showActivities.watchlisted_at),
new Func2<Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>>()
{
@Override
public Map<String, ContentValues> call(Map<String, ContentValues> mapRatings, Map<String, ContentValues> mapWatchlisted)
{
return mergeMaps(mapRatings, mapWatchlisted);
}
});
}
private Observable<Map<String, ContentValues>> getMergedSeasonObservable(LastActivity seasonActivities)
{
return Observable.zip(
getRatingsSeasonObservable(seasonActivities.rated_at),
getWatchlistedSeasonObservable(seasonActivities.watchlisted_at),
new Func2<Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>>()
{
@Override
public Map<String, ContentValues> call(Map<String, ContentValues> mapRatings, Map<String, ContentValues> mapWatchlisted)
{
return mergeMaps(mapRatings, mapWatchlisted);
}
});
}
private Observable<Map<String, ContentValues>> getMergedEpisodeObservable(LastActivityMore episodeActivities)
{
return Observable.zip(
getCollectedEpisodeObservable(episodeActivities.collected_at),
getRatingsEpisodeObservable(episodeActivities.rated_at),
getWatchedEpisodeObservable(episodeActivities.watched_at),
getWatchlistedEpisodeObservable(episodeActivities.watchlisted_at),
new Func4<Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>, Map<String, ContentValues>>()
{
@Override
public Map<String, ContentValues> call(Map<String, ContentValues> mapCollected, Map<String, ContentValues> mapRatings, Map<String, ContentValues> mapWatched, Map<String, ContentValues> mapWatchlisted)
{
return mergeMaps(mapWatched, mapCollected, mapRatings, mapWatchlisted);
}
});
}
@SafeVarargs
private final <T> Map<T, ContentValues> mergeMaps(Map<T, ContentValues>... maps)
{
Map<T, ContentValues> map = new HashMap<>();
if(maps == null || maps.length == 0)
return map;
map.putAll(maps[0]);
for(int i = 1; i < maps.length; i++)
{
for(Map.Entry<T, ContentValues> entry : maps[i].entrySet())
{
T key = entry.getKey();
ContentValues contentValues = map.get(key);
if(contentValues == null)
contentValues = new ContentValues();
contentValues.putAll(entry.getValue());
map.put(key, contentValues);
}
}
return map;
}
public void sync()
{
// update existing shows and movies
Map<String, Long> localShowMap = DbHelper.getLastUpdatedShowMap(context);
if (localShowMap.size() > 0)
{
// TODO
// List<TvShow> shows = TraktManager.getInstance().shows().updates();
}
Map<String, Long> localMovieMap = DbHelper.getLastUpdatedMovieMap(context);
if (localMovieMap.size() > 0)
{
// TODO
// List<TvShow> shows = TraktManager.getInstance().movies().updates();
}
Observable
.create(new TraktObservable<LastActivities>()
{
@Override
public LastActivities fire() throws OAuthUnauthorizedException
{
return TraktManager.getInstance().sync().lastActivities();
}
})
.retry(retryFunc)
.flatMap(new Func1<LastActivities, Observable<?>>()
{
@Override
public Observable<?> call(LastActivities lastActivities)
{
if(!isSyncNeeded(lastActivities.all))
return Observable.just(Collections.emptyMap());
return Observable.merge(
getMovieObservable(lastActivities.movies),
getShowObservable(lastActivities)
);
}
})
.doOnSubscribe(new Action0()
{
@Override
public void call()
{
startSyncTime = DateHelper.now();
lastLocalSyncTime = TraktoidPrefs.INSTANCE.getLastSyncTime();
Timber.d("startSyncTime : %s", startSyncTime);
Timber.d("lastLocalSyncTime : %s", lastLocalSyncTime);
}
})
.doOnCompleted(new Action0()
{
@Override
public void call()
{
TraktoidPrefs.INSTANCE.putLastSyncTime(startSyncTime);
}
})
.subscribe(new Observer<Object>()
{
@Override
public void onCompleted()
{
Timber.d("Sync completed");
cancelNotification();
}
@Override
public void onError(Throwable e)
{
errorHandler.handle(e, "Error during sync");
Intent intent = new Intent(context, TraktoidService.class);
PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, 0);
notificationBuilder = new NotificationCompat.Builder(context)
.setContentTitle("Traktoid")
.setContentText("Error during synchronization.")
.setSmallIcon(R.drawable.ic_sync_problem_white_24dp)
.setColor(TraktoidTheme.DEFAULT.getColorDark(context))
.addAction(new NotificationCompat.Action(R.drawable.ic_refresh_grey600_24dp, "Retry", pendingIntent));
updateNotification();
}
@Override
public void onNext(Object o)
{
}
});
}
private void updateNotification()
{
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
}
private void cancelNotification()
{
notificationManager.cancel(NOTIFICATION_ID);
}
}