package com.boardgamegeek.service; import android.accounts.Account; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SyncResult; import android.database.Cursor; import android.support.annotation.Nullable; import android.support.annotation.PluralsRes; import android.support.annotation.StringRes; import com.boardgamegeek.R; import com.boardgamegeek.auth.Authenticator; import com.boardgamegeek.io.BggService; import com.boardgamegeek.provider.BggContract; import com.boardgamegeek.provider.BggContract.Collection; import com.boardgamegeek.service.model.CollectionItem; import com.boardgamegeek.ui.CollectionActivity; import com.boardgamegeek.util.ActivityUtils; import com.boardgamegeek.util.HttpUtils; import com.boardgamegeek.util.NotificationUtils; import com.boardgamegeek.util.SelectionBuilder; import java.util.ArrayList; import java.util.List; import hugo.weaving.DebugLog; import okhttp3.OkHttpClient; import timber.log.Timber; public class SyncCollectionUpload extends SyncUploadTask { private ContentResolver resolver; private SyncResult syncResult; private final OkHttpClient okHttpClient; private final CollectionDeleteTask deleteTask; private final CollectionAddTask addTask; private final List<CollectionUploadTask> uploadTasks; private int currentGameId; private String currentGameName; @DebugLog public SyncCollectionUpload(Context context, BggService service) { super(context, service); okHttpClient = HttpUtils.getHttpClientWithAuth(context); deleteTask = new CollectionDeleteTask(okHttpClient); addTask = new CollectionAddTask(okHttpClient); uploadTasks = createUploadTasks(); } private List<CollectionUploadTask> createUploadTasks() { List<CollectionUploadTask> tasks = new ArrayList<>(); tasks.add(new CollectionStatusUploadTask(okHttpClient)); tasks.add(new CollectionRatingUploadTask(okHttpClient)); tasks.add(new CollectionCommentUploadTask(okHttpClient)); tasks.add(new CollectionPrivateInfoUploadTask(okHttpClient)); tasks.add(new CollectionWishlistCommentUploadTask(okHttpClient)); tasks.add(new CollectionTradeConditionUploadTask(okHttpClient)); tasks.add(new CollectionWantPartsUploadTask(okHttpClient)); tasks.add(new CollectionHasPartsUploadTask(okHttpClient)); return tasks; } @DebugLog @Override public int getSyncType() { return SyncService.FLAG_SYNC_COLLECTION_UPLOAD; } @DebugLog @Override protected int getNotificationTitleResId() { return R.string.sync_notification_title_collection_upload; } @DebugLog @Override protected Intent getNotificationSummaryIntent() { return new Intent(context, CollectionActivity.class); } @DebugLog @Override protected Intent getNotificationIntent() { if (currentGameId != BggContract.INVALID_ID) { return ActivityUtils.createGameIntent(currentGameId, currentGameName); } return super.getNotificationIntent(); } @DebugLog @Override protected String getNotificationMessageTag() { return NotificationUtils.TAG_UPLOAD_COLLECTION; } @DebugLog @Override protected String getNotificationErrorTag() { return NotificationUtils.TAG_UPLOAD_COLLECTION_ERROR; } @DebugLog @Override public void execute(Account account, SyncResult syncResult) { init(syncResult); Cursor cursor = null; try { cursor = fetchDeletedCollectionItems(); while (cursor != null && cursor.moveToNext()) { if (isCancelled()) break; if (wasSleepInterrupted(1000)) break; processDeletedCollectionItem(cursor); } } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } cursor = null; try { cursor = fetchNewCollectionItems(); while (cursor != null && cursor.moveToNext()) { if (isCancelled()) break; if (wasSleepInterrupted(1000)) break; processNewCollectionItem(cursor); } } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } cursor = null; try { cursor = fetchDirtyCollectionItems(); while (cursor != null && cursor.moveToNext()) { if (isCancelled()) break; if (wasSleepInterrupted(1000)) break; processDirtyCollectionItem(cursor); } } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } } private void init(SyncResult syncResult) { resolver = context.getContentResolver(); this.syncResult = syncResult; } private Cursor fetchDeletedCollectionItems() { return getCollectionItems(isGreaterThanZero(Collection.COLLECTION_DELETE_TIMESTAMP), R.plurals.sync_notification_collection_deleting); } private Cursor fetchNewCollectionItems() { String selection = "(" + getDirtyColumnSelection(isGreaterThanZero(Collection.COLLECTION_DIRTY_TIMESTAMP)) + ") AND " + SelectionBuilder.whereNullOrEmpty(Collection.COLLECTION_ID); return getCollectionItems(selection, R.plurals.sync_notification_collection_adding); } private Cursor fetchDirtyCollectionItems() { String selection = getDirtyColumnSelection(""); return getCollectionItems(selection, R.plurals.sync_notification_collection_uploading); } private String getDirtyColumnSelection(String existingSelection) { StringBuilder sb = new StringBuilder(existingSelection); for (CollectionUploadTask task : uploadTasks) { if (sb.length() > 0) sb.append(" OR "); sb.append(isGreaterThanZero(task.getTimestampColumn())); } return sb.toString(); } private static String isGreaterThanZero(String columnName) { return columnName + ">0"; } @Nullable private Cursor getCollectionItems(String selection, @PluralsRes int messageResId) { Cursor cursor = context.getContentResolver().query(Collection.CONTENT_URI, CollectionItem.PROJECTION, selection, null, null); final int count = cursor != null ? cursor.getCount() : 0; String detail = context.getResources().getQuantityString(messageResId, count, count); Timber.i(detail); updateProgressNotification(detail); return cursor; } private void processDeletedCollectionItem(Cursor cursor) { CollectionItem item = CollectionItem.fromCursor(cursor); deleteTask.addCollectionItem(item); deleteTask.post(); if (processResponseForError(deleteTask)) { return; } resolver.delete(Collection.buildUri(item.getInternalId()), null, null); notifySuccess(item, item.getCollectionId(), R.string.sync_notification_collection_deleted); } private void processNewCollectionItem(Cursor cursor) { CollectionItem item = CollectionItem.fromCursor(cursor); addTask.addCollectionItem(item); addTask.post(); if (processResponseForError(addTask)) { return; } ContentValues contentValues = new ContentValues(); addTask.appendContentValues(contentValues); resolver.update(Collection.buildUri(item.getInternalId()), contentValues, null, null); UpdateService.start(context, UpdateService.SYNC_TYPE_GAME_COLLECTION, item.getGameId()); notifySuccess(item, item.getGameId() * -1, R.string.sync_notification_collection_added); } private void processDirtyCollectionItem(Cursor cursor) { CollectionItem item = CollectionItem.fromCursor(cursor); if (item.getCollectionId() != BggContract.INVALID_ID) { ContentValues contentValues = new ContentValues(); for (CollectionUploadTask task : uploadTasks) { if (processUploadTask(task, item, contentValues)) return; } if (contentValues.size() > 0) { resolver.update(Collection.buildUri(item.getInternalId()), contentValues, null, null); notifySuccess(item, item.getCollectionId(), R.string.sync_notification_collection_updated); } } else { Timber.d("Invalid collectionItem ID for internal ID %1$s; game ID %2$s", item.getInternalId(), item.getGameId()); } } private boolean processUploadTask(CollectionUploadTask task, CollectionItem collectionItem, ContentValues contentValues) { task.addCollectionItem(collectionItem); if (task.isDirty()) { task.post(); if (processResponseForError(task)) { return true; } task.appendContentValues(contentValues); } return false; } private void notifySuccess(CollectionItem item, int id, @StringRes int messageResId) { syncResult.stats.numUpdates++; currentGameId = item.getGameId(); currentGameName = item.getCollectionName(); notifyUser(item.getCollectionName(), context.getString(messageResId), id, item.getImageUrl(), item.getThumbnailUrl()); } private boolean processResponseForError(CollectionTask response) { if (response.hasAuthError()) { Timber.w("Auth error; clearing password"); syncResult.stats.numAuthExceptions++; Authenticator.clearPassword(context); return true; } else if (response.hasError()) { syncResult.stats.numIoExceptions++; notifyUploadError(response.getErrorMessage()); return true; } return false; } }