package com.vaguehope.onosendai.provider; import java.io.FileNotFoundException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Set; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.support.v4.app.NotificationCompat; import com.vaguehope.onosendai.R; import com.vaguehope.onosendai.config.Account; import com.vaguehope.onosendai.model.Tweet; import com.vaguehope.onosendai.model.TweetBuilder; import com.vaguehope.onosendai.notifications.NotificationIds; import com.vaguehope.onosendai.notifications.Notifications; import com.vaguehope.onosendai.provider.PostTask.PostRequest; import com.vaguehope.onosendai.provider.bufferapp.BufferAppProvider; import com.vaguehope.onosendai.provider.instapaper.InstapaperProvider; import com.vaguehope.onosendai.provider.successwhale.SuccessWhaleProvider; import com.vaguehope.onosendai.provider.twitter.TwitterProvider; import com.vaguehope.onosendai.storage.DbBindingAsyncTask; import com.vaguehope.onosendai.storage.DbInterface; import com.vaguehope.onosendai.ui.OutboxActivity; import com.vaguehope.onosendai.util.ImageMetadata; import com.vaguehope.onosendai.util.LogWrapper; public class PostTask extends DbBindingAsyncTask<Void, Integer, SendResult<PostRequest>> { private static final LogWrapper LOG = new LogWrapper("PT"); private final PostRequest req; private final int notificationId; private NotificationManager notificationMgr; private NotificationCompat.Builder notificationBuilder; public PostTask (final Context context, final PostRequest req) { super(context); this.req = req; if (req.getRecoveryIntent() != null) { this.notificationId = (int) System.currentTimeMillis(); // Probably unique. } else { this.notificationId = NotificationIds.OUTBOX_NOTIFICATION_ID; } } @Override protected LogWrapper getLog () { return LOG; } @Override protected void onPreExecute () { this.notificationMgr = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE); this.notificationBuilder = new NotificationCompat.Builder(getContext()) .setSmallIcon(Notifications.notificationIcon()) .setContentTitle(String.format("Posting to %s...", this.req.getAccount().getUiTitle())) //ES .setOngoing(true) .setUsesChronometer(true); updateNotificaiton(); } private void updateNotificaiton () { this.notificationMgr.notify(this.notificationId, this.notificationBuilder.build()); } protected void setProgress (final int max, final int progress) { this.notificationBuilder.setProgress(max, progress, false); updateNotificaiton(); } @Override protected SendResult<PostRequest> doInBackgroundWithDb (final DbInterface db, final Void... params) { LOG.i("Posting: %s", this.req); switch (this.req.getAccount().getProvider()) { case TWITTER: return postTwitter(); case SUCCESSWHALE: return postSuccessWhale(db); case BUFFER: return postBufferApp(); case INSTAPAPER: return postInstapaper(); default: return new SendResult<PostRequest>(this.req, new UnsupportedOperationException("Do not know how to post to account type: " + this.req.getAccount().getUiTitle())); } } private SendResult<PostRequest> postTwitter () { final TwitterProvider p = new TwitterProvider(); try { final Tweet u = p.post(this.req.getAccount(), this.req.getBody(), this.req.getInReplyToSidLong(), resolveAttachment()); return new SendResult<PostRequest>(this.req, u); } catch (final Exception e) { // NOSONAR need to report all errors. return new SendResult<PostRequest>(this.req, e); } finally { p.shutdown(); } } private SendResult<PostRequest> postSuccessWhale (final DbInterface db) { final SuccessWhaleProvider s = new SuccessWhaleProvider(db); try { s.post(this.req.getAccount(), this.req.getPostToSvc(), this.req.getBody(), this.req.getInReplyToSid(), resolveAttachment()); return new SendResult<PostRequest>(this.req, (Tweet) null); // FIXME parse SW response for ID. } catch (final Exception e) { // NOSONAR need to report all errors. return new SendResult<PostRequest>(this.req, e); } finally { s.shutdown(); } } private SendResult<PostRequest> postBufferApp () { final BufferAppProvider b = new BufferAppProvider(); try { if (this.req.getInReplyToSid() != null) throw new IllegalArgumentException("BufferApp does not support inReplyTo."); // TODO do not get here. if (resolveAttachment() != null) throw new IllegalArgumentException("BufferApp does not support posting images."); // TODO do not get here. b.post(this.req.getAccount(), this.req.getPostToSvc(), this.req.getBody()); return new SendResult<PostRequest>(this.req); } catch (final Exception e) { // NOSONAR need to report all errors. return new SendResult<PostRequest>(this.req, e); } finally { b.shutdown(); } } private SendResult<PostRequest> postInstapaper () { final InstapaperProvider p = new InstapaperProvider(); try { if (this.req.getInReplyToSid() != null) LOG.w("Instapaper does not support inReplyTo field, ignoring it."); // TODO do not get here. if (resolveAttachment() != null) throw new IllegalArgumentException("Instapaper does not support posting images."); // TODO do not get here. final TweetBuilder b = new TweetBuilder(); b.body(this.req.getBody()); p.add(this.req.getAccount(), b.build()); return new SendResult<PostRequest>(this.req); } catch (final Exception e) { // NOSONAR need to report all errors. return new SendResult<PostRequest>(this.req, e); } finally { p.shutdown(); } } private ImageMetadata resolveAttachment () throws FileNotFoundException { if (this.req.getAttachment() == null) return null; final ImageMetadata image = new ProgressTrackingImageMetadata(this, getContext(), this.req.getAttachment()); if (!image.exists()) throw new FileNotFoundException("Attachment not found: " + this.req.getAttachment()); return image; } @Override protected void onPostExecute (final SendResult<PostRequest> res) { switch (res.getOutcome()) { case SUCCESS: case PREVIOUS_ATTEMPT_SUCCEEDED: this.notificationMgr.cancel(this.notificationId); break; default: LOG.w("Post failed (" + res.getOutcome() + ").", res.getE()); Intent intent; String title; if (this.req.getRecoveryIntent() != null) { intent = this.req.getRecoveryIntent(); title = String.format("Tap to retry post to %s.", this.req.getAccount().getUiTitle()); //ES } else { intent = new Intent(getContext(), OutboxActivity.class); title = String.format("Post to %s will be retried in background.", this.req.getAccount().getUiTitle()); //ES // TODO only one notification for all outbox issues! } final PendingIntent contentIntent = PendingIntent.getActivity(getContext(), this.notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); final Notification n = new NotificationCompat.Builder(getContext()) .setSmallIcon(R.drawable.exclamation_red) // TODO better icon. .setContentText(res.getEmsg()) .setAutoCancel(true) .setUsesChronometer(false) .setWhen(System.currentTimeMillis()) .setContentIntent(contentIntent) .setContentTitle(title) .build(); this.notificationMgr.notify(this.notificationId, n); } } public static class PostRequest { private final Account account; private final Set<ServiceRef> postToSvc; private final String body; private final String inReplyToSid; private final Uri attachment; private final Intent recoveryIntent; public PostRequest (final Account account, final Set<ServiceRef> postToSvc, final String body, final String inReplyToSid, final Uri attachment) { this(account, postToSvc, body, inReplyToSid, attachment, null); } public PostRequest (final Account account, final Set<ServiceRef> postToSvc, final String body, final String inReplyToSid, final Uri attachment, final Intent recoveryIntent) { this.account = account; this.postToSvc = postToSvc; this.body = body; this.inReplyToSid = inReplyToSid; this.attachment = attachment; this.recoveryIntent = recoveryIntent; } public Account getAccount () { return this.account; } public Set<ServiceRef> getPostToSvc () { return this.postToSvc; } public String getBody () { return this.body; } public String getInReplyToSid () { return this.inReplyToSid; } public long getInReplyToSidLong () { if (this.inReplyToSid == null || this.inReplyToSid.isEmpty()) return -1; return Long.parseLong(this.inReplyToSid); } public Uri getAttachment () { return this.attachment; } public Intent getRecoveryIntent () { return this.recoveryIntent; } @Override public String toString () { return new StringBuilder() .append("PostRequest{").append(this.account) .append(",").append(this.postToSvc) .append(",").append(this.inReplyToSid) .append(",").append(this.attachment) .append("}").toString(); } } private static class ProgressTrackingImageMetadata extends ImageMetadata { private final PostTask host; public ProgressTrackingImageMetadata (final PostTask host, final Context context, final Uri uri) { super(context, uri); this.host = host; } @Override public InputStream open () throws IOException { final InputStream is = super.open(); if (is == null) return null; return new ProgressTrackingInputStream(this.host, getSize(), is); } } private static class ProgressTrackingInputStream extends FilterInputStream { private static final float PERCENT_F = 100f; private static final int PERCENT_I = 100; private final PostTask host; private final long size; private long progress = 0; private int lastPercent = -1; protected ProgressTrackingInputStream (final PostTask host, final long size, final InputStream is) { super(is); this.host = host; this.size = size; } private void increment (final int added) { if (added < 1) return; this.progress += added; final int percent = (int) (this.progress * PERCENT_F / this.size); if (percent != this.lastPercent) { this.host.setProgress(PERCENT_I, percent); this.lastPercent = percent; } } @Override public int read () throws IOException { final int n = super.read(); increment(1); return n; } @Override public int read (final byte[] buffer, final int offset, final int count) throws IOException { final int n = super.read(buffer, offset, count); increment(n); return n; } } }