package com.boardgamegeek.service;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ComponentName;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncResult;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import com.boardgamegeek.BuildConfig;
import com.boardgamegeek.R;
import com.boardgamegeek.auth.Authenticator;
import com.boardgamegeek.events.SyncCompleteEvent;
import com.boardgamegeek.events.SyncEvent;
import com.boardgamegeek.io.Adapter;
import com.boardgamegeek.io.BggService;
import com.boardgamegeek.util.BatteryUtils;
import com.boardgamegeek.util.DateTimeUtils;
import com.boardgamegeek.util.NetworkUtils;
import com.boardgamegeek.util.NotificationUtils;
import com.boardgamegeek.util.PreferencesUtils;
import org.greenrobot.eventbus.EventBus;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
import hugo.weaving.DebugLog;
import timber.log.Timber;
public class SyncAdapter extends AbstractThreadedSyncAdapter {
private final Context context;
private boolean shouldShowNotifications = true;
private SyncTask currentTask;
private boolean isCancelled;
@DebugLog
public SyncAdapter(Context context) {
super(context, false);
this.context = context;
shouldShowNotifications = PreferencesUtils.getSyncShowNotifications(this.context);
if (!BuildConfig.DEBUG) {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
Timber.e(throwable, "Uncaught sync exception, suppressing UI in release build.");
}
});
}
}
@DebugLog
@Override
public void onPerformSync(@NonNull Account account, @NonNull Bundle extras, String authority, ContentProviderClient provider, @NonNull SyncResult syncResult) {
isCancelled = false;
final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false);
final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
final boolean initialize = extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false);
final int type = extras.getInt(SyncService.EXTRA_SYNC_TYPE, SyncService.FLAG_SYNC_ALL);
Timber.i("Beginning sync for account " + account.name + "," + " uploadOnly=" + uploadOnly + " manualSync="
+ manualSync + " initialize=" + initialize + ", type=" + type);
if (initialize) {
ContentResolver.setIsSyncable(account, authority, 1);
ContentResolver.setSyncAutomatically(account, authority, true);
Bundle b = new Bundle();
ContentResolver.addPeriodicSync(account, authority, b, 24 * 60 * 60); // 24 hours
}
if (!shouldContinueSync(uploadOnly)) {
return;
}
toggleReceiver(true);
shouldShowNotifications = PreferencesUtils.getSyncShowNotifications(context);
List<SyncTask> tasks = createTasks(context, type);
for (int i = 0; i < tasks.size(); i++) {
if (isCancelled) {
Timber.i("Cancelling all sync tasks");
if (currentTask != null) {
notifySyncIsCancelled(currentTask.getNotificationSummaryMessageId());
}
break;
}
currentTask = tasks.get(i);
try {
EventBus.getDefault().postSticky(new SyncEvent(currentTask.getSyncType()));
currentTask.updateProgressNotification();
currentTask.execute(account, syncResult);
EventBus.getDefault().post(new SyncCompleteEvent());
EventBus.getDefault().removeStickyEvent(SyncEvent.class);
} catch (Exception e) {
Timber.e(e, "Syncing " + currentTask);
syncResult.stats.numIoExceptions += 10;
showError(currentTask, e);
if (e.getCause() instanceof SocketTimeoutException) {
break;
}
}
}
toggleReceiver(false);
NotificationUtils.cancel(context, NotificationUtils.TAG_SYNC_PROGRESS);
}
@DebugLog
@Override
public void onSyncCanceled() {
super.onSyncCanceled();
Timber.i("Sync cancel requested.");
isCancelled = true;
if (currentTask != null) {
currentTask.cancel();
}
}
@DebugLog
private boolean shouldContinueSync(boolean uploadOnly) {
if (uploadOnly) {
Timber.w("Upload only, returning.");
return false;
}
if (NetworkUtils.isOffline(context)) {
Timber.i("Skipping sync; offline");
return false;
}
if (PreferencesUtils.getSyncOnlyCharging(context) && !BatteryUtils.isCharging(context)) {
Timber.i("Skipping sync; not charging");
return false;
}
if (PreferencesUtils.getSyncOnlyWifi(context) && !NetworkUtils.isOnWiFi(context)) {
Timber.i("Skipping sync; not on wifi");
return false;
}
if (BatteryUtils.isBatteryLow(context)) {
Timber.i("Skipping sync; battery low");
return false;
}
return true;
}
@DebugLog
@NonNull
private List<SyncTask> createTasks(Context context, final int type) {
BggService service = Adapter.createForXmlWithAuth(context);
List<SyncTask> tasks = new ArrayList<>();
if ((type & SyncService.FLAG_SYNC_COLLECTION_UPLOAD) == SyncService.FLAG_SYNC_COLLECTION_UPLOAD) {
tasks.add(new SyncCollectionUpload(context, service));
}
if ((type & SyncService.FLAG_SYNC_COLLECTION) == SyncService.FLAG_SYNC_COLLECTION) {
if (PreferencesUtils.isSyncStatus(context)) {
long lastCompleteSync = Authenticator.getLong(context, SyncService.TIMESTAMP_COLLECTION_COMPLETE);
if (lastCompleteSync >= 0 && DateTimeUtils.howManyDaysOld(lastCompleteSync) < 7) {
tasks.add(new SyncCollectionModifiedSince(context, service));
} else {
tasks.add(new SyncCollectionComplete(context, service));
}
} else {
Timber.i("...no statuses set to sync");
}
tasks.add(new SyncCollectionUnupdated(context, service));
tasks.add(new SyncGamesRemove(context, service));
tasks.add(new SyncGamesOldest(context, service));
tasks.add(new SyncGamesUnupdated(context, service));
}
if ((type & SyncService.FLAG_SYNC_PLAYS_UPLOAD) == SyncService.FLAG_SYNC_PLAYS_UPLOAD) {
tasks.add(new SyncPlaysUpload(context, service));
}
if ((type & SyncService.FLAG_SYNC_PLAYS_DOWNLOAD) == SyncService.FLAG_SYNC_PLAYS_DOWNLOAD) {
tasks.add(new SyncPlays(context, service));
}
if ((type & SyncService.FLAG_SYNC_BUDDIES) == SyncService.FLAG_SYNC_BUDDIES) {
tasks.add(new SyncBuddiesList(context, service));
tasks.add(new SyncBuddiesDetailOldest(context, service));
tasks.add(new SyncBuddiesDetailUnupdated(context, service));
}
return tasks;
}
@DebugLog
private void toggleReceiver(boolean enable) {
ComponentName receiver = new ComponentName(context, CancelReceiver.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(receiver, enable ?
PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
}
@DebugLog
private void showError(@NonNull SyncTask task, @NonNull Throwable t) {
String message = t.getMessage();
if (TextUtils.isEmpty(message)) {
Throwable t1 = t.getCause();
if (t1 != null) {
message = t1.toString();
}
}
Timber.w(message);
if (!PreferencesUtils.getSyncShowErrors(context)) return;
final int messageId = task.getNotificationSummaryMessageId();
if (messageId != ServiceTask.NO_NOTIFICATION) {
CharSequence text = context.getText(messageId);
NotificationCompat.Builder builder = NotificationUtils
.createNotificationBuilder(context, R.string.sync_notification_title_error)
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ERROR);
if (!TextUtils.isEmpty(message)) {
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(message).setSummaryText(text));
}
NotificationUtils.notify(context, NotificationUtils.TAG_SYNC_ERROR, 0, builder);
}
}
@DebugLog
private void notifySyncIsCancelled(int messageId) {
if (!shouldShowNotifications) {
return;
}
CharSequence contextText = "";
if (messageId != SyncTask.NO_NOTIFICATION) {
contextText = context.getText(messageId);
}
NotificationCompat.Builder builder = NotificationUtils
.createNotificationBuilder(context, R.string.sync_notification_title_cancel)
.setContentText(contextText)
.setCategory(NotificationCompat.CATEGORY_SERVICE);
NotificationUtils.notify(context, NotificationUtils.TAG_SYNC_PROGRESS, 0, builder);
}
}