package com.boardgamegeek.service;
import android.accounts.Account;
import android.app.PendingIntent;
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.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Action;
import android.util.Pair;
import com.boardgamegeek.R;
import com.boardgamegeek.auth.Authenticator;
import com.boardgamegeek.io.BggService;
import com.boardgamegeek.model.Play;
import com.boardgamegeek.model.PlayDeleteResponse;
import com.boardgamegeek.model.PlaySaveResponse;
import com.boardgamegeek.model.Player;
import com.boardgamegeek.model.builder.PlayBuilder;
import com.boardgamegeek.model.persister.PlayPersister;
import com.boardgamegeek.provider.BggContract;
import com.boardgamegeek.provider.BggContract.Collection;
import com.boardgamegeek.provider.BggContract.Games;
import com.boardgamegeek.provider.BggContract.Plays;
import com.boardgamegeek.ui.PlaysActivity;
import com.boardgamegeek.util.ActivityUtils;
import com.boardgamegeek.util.CursorUtils;
import com.boardgamegeek.util.HttpUtils;
import com.boardgamegeek.util.NotificationUtils;
import com.boardgamegeek.util.PresentationUtils;
import com.boardgamegeek.util.SelectionBuilder;
import com.boardgamegeek.util.StringUtils;
import java.util.List;
import hugo.weaving.DebugLog;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Request.Builder;
public class SyncPlaysUpload extends SyncUploadTask {
public static final String GEEK_PLAY_URL = "https://www.boardgamegeek.com/geekplay.php";
private OkHttpClient httpClient;
private PlayPersister persister;
private long currentInternalId;
private int currentGameIdForNotification;
private String currentGameNameForNotification;
private String currentThumbnailUrlForNotification;
private String currentImageUrlForNotification;
@DebugLog
public SyncPlaysUpload(Context context, BggService service) {
super(context, service);
}
@DebugLog
@Override
public int getSyncType() {
return SyncService.FLAG_SYNC_PLAYS_UPLOAD;
}
@DebugLog
@Override
protected int getNotificationTitleResId() {
return R.string.sync_notification_title_play_upload;
}
@DebugLog
@Override
protected Intent getNotificationSummaryIntent() {
return new Intent(context, PlaysActivity.class);
}
@DebugLog
@Override
protected Intent getNotificationIntent() {
if (currentInternalId == BggContract.INVALID_ID) {
return ActivityUtils.createGamePlaysIntent(context,
Games.buildGameUri(currentGameIdForNotification),
currentGameNameForNotification,
currentImageUrlForNotification,
currentThumbnailUrlForNotification);
}
return ActivityUtils.createPlayIntent(context,
currentInternalId,
currentGameIdForNotification,
currentGameNameForNotification,
currentThumbnailUrlForNotification,
currentImageUrlForNotification);
}
@DebugLog
@Override
protected String getNotificationMessageTag() {
return NotificationUtils.TAG_UPLOAD_PLAY;
}
@DebugLog
@Override
protected String getNotificationErrorTag() {
return NotificationUtils.TAG_UPLOAD_PLAY_ERROR;
}
@DebugLog
@Override
public void execute(Account account, @NonNull SyncResult syncResult) {
httpClient = HttpUtils.getHttpClientWithAuth(context);
persister = new PlayPersister(context);
deletePendingPlays(syncResult);
updatePendingPlays(syncResult);
if (SyncService.isPlaysSyncUpToDate(context)) {
SyncService.calculateAndUpdateHIndex(context);
}
}
@DebugLog
@Override
public int getNotificationSummaryMessageId() {
return R.string.sync_notification_plays_upload;
}
@DebugLog
private void updatePendingPlays(@NonNull SyncResult syncResult) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(Plays.CONTENT_SIMPLE_URI,
PlayBuilder.PLAY_PROJECTION_WITH_ID,
Plays.UPDATE_TIMESTAMP + ">0",
null,
Plays.UPDATE_TIMESTAMP);
int playCount = cursor != null ? cursor.getCount() : 0;
updateProgressNotificationAsPlural(R.plurals.sync_notification_progress_update, playCount, playCount);
while (cursor != null && cursor.moveToNext()) {
if (isCancelled()) break;
if (wasSleepInterrupted(1000)) break;
currentInternalId = CursorUtils.getLong(cursor, Plays._ID, BggContract.INVALID_ID);
Play play = PlayBuilder.fromCursor(cursor);
Cursor playerCursor = PlayBuilder.queryPlayers(context, currentInternalId);
try {
PlayBuilder.addPlayers(playerCursor, play);
} finally {
if (playerCursor != null) playerCursor.close();
}
PlaySaveResponse response = postPlayUpdate(play);
if (response == null) {
syncResult.stats.numIoExceptions++;
notifyUploadError(context.getString(R.string.msg_play_update_null_response));
} else if (response.hasAuthError()) {
syncResult.stats.numAuthExceptions++;
Authenticator.clearPassword(context);
break;
} else if (response.hasInvalidIdError()) {
syncResult.stats.numConflictDetectedExceptions++;
notifyUploadError(PresentationUtils.getText(context, R.string.msg_play_update_bad_id, play.playId));
} else if (response.hasError()) {
syncResult.stats.numIoExceptions++;
notifyUploadError(response.getErrorMessage());
} else if (response.getPlayCount() <= 0) {
syncResult.stats.numIoExceptions++;
notifyUploadError(context.getString(R.string.msg_play_update_null_response));
} else {
syncResult.stats.numUpdates++;
CharSequence message = play.playId > 0 ?
PresentationUtils.getText(context, R.string.msg_play_updated) :
PresentationUtils.getText(context, R.string.msg_play_added, getPlayCountDescription(response.getPlayCount(), play.quantity));
play.playId = response.getPlayId();
play.dirtyTimestamp = 0;
play.updateTimestamp = 0;
play.deleteTimestamp = 0;
currentGameIdForNotification = play.gameId;
currentGameNameForNotification = play.gameName;
notifyUser(play, message);
persister.save(play, currentInternalId, false);
updateGamePlayCount(play);
}
}
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}
private void notifyUser(Play play, CharSequence message) {
Pair<String, String> imageUrls = queryGameImageUrls(play);
currentImageUrlForNotification = imageUrls.first;
currentThumbnailUrlForNotification = imageUrls.second;
notifyUser(play.gameName, message, NotificationUtils.getIntegerId(currentInternalId), currentImageUrlForNotification, currentThumbnailUrlForNotification);
}
private Pair<String, String> queryGameImageUrls(Play play) {
Pair<String, String> imageUrls = Pair.create("", "");
Cursor gameCursor = context.getContentResolver().query(Games.buildGameUri(play.gameId),
new String[] { Games.IMAGE_URL, Games.THUMBNAIL_URL }, null, null, null);
try {
if (gameCursor != null && gameCursor.moveToFirst()) {
imageUrls = Pair.create(gameCursor.getString(0), gameCursor.getString(1));
}
} finally {
if (gameCursor != null) gameCursor.close();
}
return imageUrls;
}
@DebugLog
private void deletePendingPlays(@NonNull SyncResult syncResult) {
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(Plays.CONTENT_SIMPLE_URI,
PlayBuilder.PLAY_PROJECTION_WITH_ID,
Plays.DELETE_TIMESTAMP + ">0",
null,
Plays.DELETE_TIMESTAMP);
int playCount = cursor != null ? cursor.getCount() : 0;
updateProgressNotificationAsPlural(R.plurals.sync_notification_progress_delete, playCount, playCount);
while (cursor != null && cursor.moveToNext()) {
if (isCancelled()) break;
currentInternalId = CursorUtils.getLong(cursor, Plays._ID, BggContract.INVALID_ID);
Play play = PlayBuilder.fromCursor(cursor);
currentGameIdForNotification = play.gameId;
currentGameNameForNotification = play.gameName;
if (play.playId > 0) {
if (wasSleepInterrupted(1000)) break;
PlayDeleteResponse response = postPlayDelete(play.playId);
if (response == null) {
syncResult.stats.numIoExceptions++;
notifyUploadError(context.getString(R.string.msg_play_update_null_response));
} else if (response.isSuccessful()) {
syncResult.stats.numDeletes++;
deletePlay(currentInternalId);
updateGamePlayCount(play);
notifyUserOfDelete(R.string.msg_play_deleted, play);
} else if (response.hasInvalidIdError()) {
syncResult.stats.numConflictDetectedExceptions++;
deletePlay(currentInternalId);
notifyUserOfDelete(R.string.msg_play_deleted, play);
} else if (response.hasAuthError()) {
syncResult.stats.numAuthExceptions++;
Authenticator.clearPassword(context);
} else {
syncResult.stats.numIoExceptions++;
notifyUploadError(response.getErrorMessage());
}
} else {
syncResult.stats.numDeletes++;
deletePlay(currentInternalId);
notifyUserOfDelete(R.string.msg_play_deleted_draft, play);
}
}
} finally {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
}
@DebugLog
private void updateGamePlayCount(@NonNull Play play) {
ContentResolver resolver = context.getContentResolver();
Cursor cursor = null;
try {
cursor = resolver.query(Plays.CONTENT_SIMPLE_URI,
new String[] { Plays.SUM_QUANTITY },
String.format("%s=? AND %s", Plays.OBJECT_ID, SelectionBuilder.whereZeroOrNull(Plays.DELETE_TIMESTAMP)),
new String[] { String.valueOf(play.gameId) },
null);
if (cursor != null && cursor.moveToFirst()) {
int newPlayCount = cursor.getInt(0);
ContentValues values = new ContentValues();
values.put(Collection.NUM_PLAYS, newPlayCount);
resolver.update(Games.buildGameUri(play.gameId), values, null, null);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@DebugLog
@Nullable
private PlaySaveResponse postPlayUpdate(@NonNull Play play) {
FormBody.Builder builder = new FormBody.Builder()
.add("ajax", "1")
.add("action", "save")
.add("version", "2")
.add("objecttype", "thing");
if (play.playId > 0) {
builder.add("playid", String.valueOf(play.playId));
}
builder.add("objectid", String.valueOf(play.gameId))
.add("playdate", play.getDate())
.add("dateinput", play.getDate())
.add("length", String.valueOf(play.length))
.add("location", play.location)
.add("quantity", String.valueOf(play.quantity))
.add("incomplete", play.Incomplete() ? "1" : "0")
.add("nowinstats", play.NoWinStats() ? "1" : "0")
.add("comments", play.comments);
List<Player> players = play.getPlayers();
for (int i = 0; i < players.size(); i++) {
Player player = players.get(i);
builder
.add(getMapKey(i, "playerid"), "player_" + i)
.add(getMapKey(i, "name"), player.name)
.add(getMapKey(i, "username"), player.username)
.add(getMapKey(i, "color"), player.color)
.add(getMapKey(i, "position"), player.startposition)
.add(getMapKey(i, "score"), player.score)
.add(getMapKey(i, "rating"), String.valueOf(player.rating))
.add(getMapKey(i, "new"), String.valueOf(player.new_))
.add(getMapKey(i, "win"), String.valueOf(player.win));
}
Request request = new Builder()
.url(GEEK_PLAY_URL)
.post(builder.build())
.build();
return new PlaySaveResponse(httpClient, request);
}
@DebugLog
@NonNull
private static String getMapKey(int index, String key) {
return "players[" + index + "][" + key + "]";
}
@DebugLog
@Nullable
private PlayDeleteResponse postPlayDelete(int playId) {
FormBody.Builder builder = new FormBody.Builder()
.add("ajax", "1")
.add("action", "delete")
.add("playid", String.valueOf(playId))
.add("finalize", "1");
Request request = new Builder()
.url(GEEK_PLAY_URL)
.post(builder.build())
.build();
return new PlayDeleteResponse(httpClient, request);
}
/**
* Deletes the specified play from the content provider
*/
@DebugLog
private void deletePlay(long internalId) {
persister.delete(internalId);
}
@DebugLog
private String getPlayCountDescription(int count, int quantity) {
String countDescription;
switch (quantity) {
case 1:
countDescription = StringUtils.getOrdinal(count);
break;
case 2:
countDescription = StringUtils.getOrdinal(count - 1) + " & " + StringUtils.getOrdinal(count);
break;
default:
countDescription = StringUtils.getOrdinal(count - quantity + 1) + " - " + StringUtils.getOrdinal(count);
break;
}
return countDescription;
}
@DebugLog
private void notifyUserOfDelete(@StringRes int messageId, Play play) {
NotificationUtils.cancel(context, getNotificationMessageTag(), NotificationUtils.getIntegerId(currentInternalId));
currentInternalId = BggContract.INVALID_ID;
notifyUser(play, PresentationUtils.getText(context, messageId, play.gameName));
}
@DebugLog
@Override
protected Action createMessageAction() {
if (currentInternalId != BggContract.INVALID_ID) {
Intent intent = ActivityUtils.createRematchIntent(context,
currentInternalId,
currentGameIdForNotification,
currentGameNameForNotification, null, null);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action.Builder builder = new NotificationCompat.Action.Builder(
R.drawable.ic_replay_black_24dp,
context.getString(R.string.rematch),
pendingIntent);
return builder.build();
}
return null;
}
}