package com.vaguehope.onosendai.provider; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutorService; import android.content.Intent; import android.os.AsyncTask; import com.vaguehope.onosendai.C; import com.vaguehope.onosendai.config.Config; import com.vaguehope.onosendai.config.Prefs; import com.vaguehope.onosendai.model.OutboxTweet; import com.vaguehope.onosendai.model.OutboxTweet.OutboxAction; import com.vaguehope.onosendai.model.OutboxTweet.OutboxTweetStatus; import com.vaguehope.onosendai.provider.OutboxTask.OtRequest; import com.vaguehope.onosendai.provider.PostTask.PostRequest; import com.vaguehope.onosendai.storage.DbBindingService; import com.vaguehope.onosendai.util.LogWrapper; import com.vaguehope.onosendai.util.StringHelper; import com.vaguehope.onosendai.util.exec.ExecUtils; public class SendOutboxService extends DbBindingService { public static final String ARG_TRY_SEND_ALL = "try_send_all"; protected static final LogWrapper LOG = new LogWrapper("SS"); private ExecutorService es; public SendOutboxService () { super("OnosendaiSendOutboxService", LOG); } @Override public void onCreate () { super.onCreate(); this.es = ExecUtils.newBoundedCachedThreadPool(C.SEND_OUTBOX_MAX_THREADS, LOG); } @Override public void onDestroy () { if (this.es != null) this.es.shutdown(); super.onDestroy(); } @Override protected void doWork (final Intent i) { if (!waitForDbReady()) return; final List<OutboxTweet> entries = getDb().getOutboxEntries(OutboxTweetStatus.PENDING); if (entries.size() < 1) { LOG.d("No pending tweets to send."); return; } LOG.d("Entries to send: %s...", entries.size()); final Config conf; try { final Prefs prefs = new Prefs(getBaseContext()); conf = prefs.asConfig(); } catch (final Exception e) { // No point continuing if any exception. LOG.e("Failed to read config.", e); // FIXME replace with something the user will actually see. return; } final boolean trySendAll = i.getBooleanExtra(ARG_TRY_SEND_ALL, false); for (final OutboxTweet ot : entries) { if (!trySendAll && ot.getStatusTime() != null && ot.getAttemptCount() > 0) { final long toWait = C.SEND_OUTBOX_RETRY_WAIT_MILLIS - (System.currentTimeMillis() - ot.getStatusTime()); if (toWait > 0L) { LOG.i("Waiting at least %sms before retrying: %s", toWait, ot); continue; } } final AsyncTask<Void, ?, ? extends SendResult<?>> task = makeTask(conf, ot); if (task != null) executeTask(ot, task); } } private AsyncTask<Void, ?, ? extends SendResult<?>> makeTask (final Config conf, final OutboxTweet ot) { switch (ot.getAction()) { case POST: if (OutboxTweet.isTempSid(ot.getInReplyToSid())) { return makePostTaskReplacingTempSid(conf, ot); } else { return new PostTask(getApplicationContext(), outboxTweetToPostRequest(ot, conf)); } case RT: return new OutboxTask(getApplicationContext(), outboxTweetToOtRequest(OutboxAction.RT, ot, conf)); case FAV: return new OutboxTask(getApplicationContext(), outboxTweetToOtRequest(OutboxAction.FAV, ot, conf)); case DELETE: return new OutboxTask(getApplicationContext(), outboxTweetToOtRequest(OutboxAction.DELETE, ot, conf)); default: throw new IllegalStateException("Do not know how to process action: " + ot.getAction()); } } private AsyncTask<Void, ?, ? extends SendResult<?>> makePostTaskReplacingTempSid (final Config conf, final OutboxTweet ot) { final long inReplyToObId = OutboxTweet.uidFromTempSid(ot.getInReplyToSid()); final OutboxTweet inReplyToObEntry = getDb().getOutboxEntry(inReplyToObId); if (inReplyToObEntry != null) { if (inReplyToObEntry.getStatus() == OutboxTweetStatus.SENT) { if (StringHelper.notEmpty(inReplyToObEntry.getSid())) { if (inReplyToObEntry.getStatusTime() != null) { final long toWait = C.SEND_OUTBOX_THREAD_POST_RATE_LIMIT_MILLIS - (System.currentTimeMillis() - inReplyToObEntry.getStatusTime()); if (toWait > 0) { LOG.i("Sleeping %sms to rate limit posting.", toWait); try { Thread.sleep(toWait); } catch (InterruptedException e) {/* Ignore. */} } } return new PostTask(getApplicationContext(), outboxTweetToPostRequest( ot.withInReplyToSid(inReplyToObEntry.getSid()), conf)); } else { LOG.w("Unable to send, inReplyToObEntry missing SID: %s", inReplyToObEntry); return null; } } else { LOG.d("Unable to send, temp SID not yet sent: %s", ot); return null; } } else { LOG.w("Unable to send, temp SID not in DB: %s", ot); return null; } } private static PostRequest outboxTweetToPostRequest (final OutboxTweet ot, final Config conf) { return new PostRequest(conf.getAccount(ot.getAccountId()), ot.getSvcMetasParsed(), ot.getBody(), ot.getInReplyToSid(), ot.getAttachment()); } private static OtRequest outboxTweetToOtRequest (final OutboxAction action, final OutboxTweet ot, final Config conf) { return new OtRequest(action, ot.getUid(), conf.getAccount(ot.getAccountId()), atMostOne(ot.getSvcMetasParsed()), ot.getInReplyToSid()); } private static <T> T atMostOne (final Set<T> set) { if (set == null || set.size() < 1) return null; if (set.size() == 1) return set.iterator().next(); throw new IllegalStateException("Expected set " + set + " to contain at most one entry."); } private void executeTask (final OutboxTweet ot, final AsyncTask<Void, ?, ? extends SendResult<?>> task) { task.executeOnExecutor(this.es); OutboxTweet otStat = null; try { final SendResult<?> res = task.get(); switch (res.getOutcome()) { case SUCCESS: case PREVIOUS_ATTEMPT_SUCCEEDED: final String sid = res.getResponse() != null ? res.getResponse().getSid() : null; LOG.i("Sent (%s, sid=%s): %s", res.getOutcome(), sid, ot); getDb().updateOutboxEntry(ot.markAsSent(sid)); break; case TEMPORARY_FAILURE: otStat = ot.tempFailure(res.getEmsg()); break; default: otStat = ot.permFailure(res.getEmsg()); } } catch (final Exception e) { // NOSONAR report all errors. switch (TaskUtils.failureType(e)) { case TEMPORARY_FAILURE: otStat = ot.tempFailure(e.toString()); break; default: otStat = ot.permFailure(e.toString()); } } if (otStat != null) { LOG.w("Send failed: %s", otStat.getLastError()); getDb().updateOutboxEntry(otStat); } } }