/* * Firetweet - Twitter client for Android * * Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.getlantern.firetweet.service; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Handler; import android.os.Parcelable; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat.Builder; import android.util.Log; import android.widget.Toast; import com.nostra13.universalimageloader.utils.IoUtils; import com.twitter.Extractor; import org.getlantern.querybuilder.Expression; import org.getlantern.firetweet.Constants; import org.getlantern.firetweet.R; import org.getlantern.firetweet.activity.MainActivity; import org.getlantern.firetweet.activity.MainHondaJOJOActivity; import org.getlantern.firetweet.app.FiretweetApplication; import org.getlantern.firetweet.model.MediaUploadResult; import org.getlantern.firetweet.model.ParcelableAccount; import org.getlantern.firetweet.model.ParcelableDirectMessage; import org.getlantern.firetweet.model.ParcelableLocation; import org.getlantern.firetweet.model.ParcelableMedia; import org.getlantern.firetweet.model.ParcelableMediaUpdate; import org.getlantern.firetweet.model.ParcelableStatus; import org.getlantern.firetweet.model.ParcelableStatusUpdate; import org.getlantern.firetweet.model.SingleResponse; import org.getlantern.firetweet.model.StatusShortenResult; import org.getlantern.firetweet.model.UploaderMediaItem; import org.getlantern.firetweet.preference.ServicePickerPreference; import org.getlantern.firetweet.provider.FiretweetDataStore.CachedHashtags; import org.getlantern.firetweet.provider.FiretweetDataStore.DirectMessages; import org.getlantern.firetweet.provider.FiretweetDataStore.Drafts; import org.getlantern.firetweet.util.AsyncTwitterWrapper; import org.getlantern.firetweet.util.BitmapUtils; import org.getlantern.firetweet.util.ContentValuesCreator; import org.getlantern.firetweet.util.ListUtils; import org.getlantern.firetweet.util.MediaUploaderInterface; import org.getlantern.firetweet.util.MessagesManager; import org.getlantern.firetweet.util.ParseUtils; import org.getlantern.firetweet.util.StatusCodeMessageUtils; import org.getlantern.firetweet.util.StatusShortenerInterface; import org.getlantern.firetweet.util.FiretweetValidator; import org.getlantern.firetweet.util.Utils; import org.getlantern.firetweet.util.io.ContentLengthInputStream; import org.getlantern.firetweet.util.io.ContentLengthInputStream.ReadListener; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import twitter4j.MediaUploadResponse; import twitter4j.Status; import twitter4j.StatusUpdate; import twitter4j.Twitter; import twitter4j.TwitterException; import twitter4j.UserMentionEntity; import com.crashlytics.android.Crashlytics; import static android.text.TextUtils.isEmpty; import static org.getlantern.firetweet.util.ContentValuesCreator.createMessageDraft; import static org.getlantern.firetweet.util.Utils.getImagePathFromUri; import static org.getlantern.firetweet.util.Utils.getImageUploadStatus; import static org.getlantern.firetweet.util.Utils.getTwitterInstance; public class BackgroundOperationService extends IntentService implements Constants { private FiretweetValidator mValidator; private final Extractor extractor = new Extractor(); private Handler mHandler; private SharedPreferences mPreferences; private ContentResolver mResolver; private NotificationManager mNotificationManager; private AsyncTwitterWrapper mTwitter; private MessagesManager mMessagesManager; private MediaUploaderInterface mUploader; private StatusShortenerInterface mShortener; private boolean mUseUploader, mUseShortener; public BackgroundOperationService() { super("background_operation"); } @Override public void onCreate() { super.onCreate(); final FiretweetApplication app = FiretweetApplication.getInstance(this); mHandler = new Handler(); mPreferences = getSharedPreferences(SHARED_PREFERENCES_NAME, MODE_PRIVATE); mValidator = new FiretweetValidator(this); mResolver = getContentResolver(); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mTwitter = app.getTwitterWrapper(); mMessagesManager = app.getMessagesManager(); final String uploaderComponent = mPreferences.getString(KEY_MEDIA_UPLOADER, null); final String shortenerComponent = mPreferences.getString(KEY_STATUS_SHORTENER, null); mUseUploader = !ServicePickerPreference.isNoneValue(uploaderComponent); mUseShortener = !ServicePickerPreference.isNoneValue(shortenerComponent); mUploader = mUseUploader ? MediaUploaderInterface.getInstance(app, uploaderComponent) : null; mShortener = mUseShortener ? StatusShortenerInterface.getInstance(app, shortenerComponent) : null; } @Override public int onStartCommand(final Intent intent, final int flags, final int startId) { super.onStartCommand(intent, flags, startId); return START_STICKY; } public void showErrorMessage(final CharSequence message, final boolean long_message) { mHandler.post(new Runnable() { @Override public void run() { mMessagesManager.showErrorMessage(message, long_message); } }); } public void showErrorMessage(final int action_res, final Exception e, final boolean long_message) { mHandler.post(new Runnable() { @Override public void run() { mMessagesManager.showErrorMessage(action_res, e, long_message); } }); } public void showErrorMessage(final int action_res, final String message, final boolean long_message) { mHandler.post(new Runnable() { @Override public void run() { mMessagesManager.showErrorMessage(action_res, message, long_message); } }); } public void showOkMessage(final int message_res, final boolean long_message) { mHandler.post(new Runnable() { @Override public void run() { mMessagesManager.showOkMessage(message_res, long_message); } }); } @Override protected void onHandleIntent(final Intent intent) { if (intent == null) return; final String action = intent.getAction(); switch (action) { case INTENT_ACTION_UPDATE_STATUS: handleUpdateStatusIntent(intent); break; case INTENT_ACTION_SEND_DIRECT_MESSAGE: handleSendDirectMessageIntent(intent); break; case INTENT_ACTION_DISCARD_DRAFT: handleDiscardDraftIntent(intent); break; } } private void handleDiscardDraftIntent(Intent intent) { final Uri data = intent.getData(); if (data == null) return; mNotificationManager.cancel(data.toString(), NOTIFICATION_ID_DRAFTS); final ContentResolver contentResolver = getContentResolver(); final long id = ParseUtils.parseLong(data.getLastPathSegment(), -1); final Expression where = Expression.equals(Drafts._ID, id); contentResolver.delete(Drafts.CONTENT_URI, where.getSQL(), null); } private Notification buildNotification(final String title, final String message, final int icon, final Intent content_intent, final Intent deleteIntent) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setTicker(message); builder.setContentTitle(title); builder.setContentText(message); builder.setAutoCancel(true); builder.setWhen(System.currentTimeMillis()); builder.setSmallIcon(icon); if (deleteIntent != null) { builder.setDeleteIntent(PendingIntent.getBroadcast(this, 0, deleteIntent, PendingIntent.FLAG_UPDATE_CURRENT)); } if (content_intent != null) { content_intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); builder.setContentIntent(PendingIntent.getActivity(this, 0, content_intent, PendingIntent.FLAG_UPDATE_CURRENT)); } // final Uri defRingtone = // RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); // final String path = // mPreferences.getString(PREFERENCE_KEY_NOTIFICATION_RINGTONE, ""); // builder.setSound(isEmpty(path) ? defRingtone : Uri.parse(path), // Notification.STREAM_DEFAULT); // builder.setLights(HOLO_BLUE_LIGHT, 1000, 2000); // builder.setDefaults(Notification.DEFAULT_VIBRATE); return builder.build(); } private void handleSendDirectMessageIntent(final Intent intent) { final long accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1); final long recipientId = intent.getLongExtra(EXTRA_RECIPIENT_ID, -1); final String imageUri = intent.getStringExtra(EXTRA_IMAGE_URI); final String text = intent.getStringExtra(EXTRA_TEXT); if (accountId <= 0 || recipientId <= 0 || isEmpty(text)) return; final String title = getString(R.string.sending_direct_message); final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); builder.setSmallIcon(R.drawable.ic_stat_send); builder.setProgress(100, 0, true); builder.setTicker(title); builder.setContentTitle(title); builder.setContentText(text); builder.setOngoing(true); final Notification notification = builder.build(); startForeground(NOTIFICATION_ID_SEND_DIRECT_MESSAGE, notification); final SingleResponse<ParcelableDirectMessage> result = sendDirectMessage(builder, accountId, recipientId, text, imageUri); if (result.getData() != null && result.getData().id > 0) { final ContentValues values = ContentValuesCreator.createDirectMessage(result.getData()); final String delete_where = DirectMessages.ACCOUNT_ID + " = " + accountId + " AND " + DirectMessages.MESSAGE_ID + " = " + result.getData().id; mResolver.delete(DirectMessages.Outbox.CONTENT_URI, delete_where, null); mResolver.insert(DirectMessages.Outbox.CONTENT_URI, values); showOkMessage(R.string.direct_message_sent, false); } else { final ContentValues values = createMessageDraft(accountId, recipientId, text, imageUri); mResolver.insert(Drafts.CONTENT_URI, values); showErrorMessage(R.string.action_sending_direct_message, result.getException(), true); } stopForeground(false); mNotificationManager.cancel(NOTIFICATION_ID_SEND_DIRECT_MESSAGE); } private void handleUpdateStatusIntent(final Intent intent) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(this); final ParcelableStatusUpdate status = intent.getParcelableExtra(EXTRA_STATUS); final Parcelable[] status_parcelables = intent.getParcelableArrayExtra(EXTRA_STATUSES); final ParcelableStatusUpdate[] statuses; if (status_parcelables != null) { statuses = new ParcelableStatusUpdate[status_parcelables.length]; for (int i = 0, j = status_parcelables.length; i < j; i++) { statuses[i] = (ParcelableStatusUpdate) status_parcelables[i]; } } else if (status != null) { statuses = new ParcelableStatusUpdate[1]; statuses[0] = status; } else return; startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotificaion(this, builder, 0, null)); for (final ParcelableStatusUpdate item : statuses) { mNotificationManager.notify(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotificaion(this, builder, 0, item)); final ContentValues draftValues = ContentValuesCreator.createStatusDraft(item, ParcelableAccount.getAccountIds(item.accounts)); final Uri draftUri = mResolver.insert(Drafts.CONTENT_URI, draftValues); final long draftId = ParseUtils.parseLong(draftUri.getLastPathSegment(), -1); mTwitter.addSendingDraftId(draftId); final List<SingleResponse<ParcelableStatus>> result = updateStatus(builder, item); boolean failed = false; Exception exception = null; final Expression where = Expression.equals(Drafts._ID, draftId); final List<Long> failedAccountIds = ListUtils.fromArray(ParcelableAccount.getAccountIds(item.accounts)); for (final SingleResponse<ParcelableStatus> response : result) { if (response.getData() == null) { failed = true; if (exception == null) { exception = response.getException(); } } else if (response.getData().account_id > 0) { failedAccountIds.remove(response.getData().account_id); } } if (result.isEmpty()) { showErrorMessage(R.string.action_updating_status, getString(R.string.no_account_selected), false); } else if (failed) { // If the status is a duplicate, there's no need to save it to // drafts. if (exception instanceof TwitterException && ((TwitterException) exception).getErrorCode() == StatusCodeMessageUtils.STATUS_IS_DUPLICATE) { showErrorMessage(getString(R.string.status_is_duplicate), false); } else { final ContentValues accountIdsValues = new ContentValues(); accountIdsValues.put(Drafts.ACCOUNT_IDS, ListUtils.toString(failedAccountIds, ',', false)); mResolver.update(Drafts.CONTENT_URI, accountIdsValues, where.getSQL(), null); showErrorMessage(R.string.action_updating_status, exception, true); displayTweetNotSendNotification(); } } else { showOkMessage(R.string.status_updated, false); mResolver.delete(Drafts.CONTENT_URI, where.getSQL(), null); if (item.media != null) { for (final ParcelableMediaUpdate media : item.media) { final String path = getImagePathFromUri(this, Uri.parse(media.uri)); if (path != null) { if (!new File(path).delete()) { Log.d(LOGTAG, String.format("unable to delete %s", path)); } } } } } mTwitter.removeSendingDraftId(draftId); if (mPreferences.getBoolean(KEY_REFRESH_AFTER_TWEET, false)) { mHandler.post(new Runnable() { @Override public void run() { mTwitter.refreshAll(); } }); } } stopForeground(false); mNotificationManager.cancel(NOTIFICATION_ID_UPDATE_STATUS); } private void displayTweetNotSendNotification() { final String title = getString(R.string.status_not_updated); final String message = getString(R.string.status_not_updated_summary); final Intent intent = new Intent(INTENT_ACTION_DRAFTS); final Notification notification = buildNotification(title, message, R.drawable.ic_stat_twitter, intent, null); mNotificationManager.notify(NOTIFICATION_ID_DRAFTS, notification); } private SingleResponse<ParcelableDirectMessage> sendDirectMessage(final NotificationCompat.Builder builder, final long accountId, final long recipientId, final String text, final String imageUri) { final Twitter twitter = getTwitterInstance(this, accountId, true, true); if (twitter == null) return SingleResponse.getInstance(); try { final ParcelableDirectMessage directMessage; if (imageUri != null) { final String path = getImagePathFromUri(this, Uri.parse(imageUri)); if (path == null) throw new FileNotFoundException(); final BitmapFactory.Options o = new BitmapFactory.Options(); o.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, o); final File file = new File(path); BitmapUtils.downscaleImageIfNeeded(file, 100); final ContentLengthInputStream is = new ContentLengthInputStream(file); is.setReadListener(new MessageMediaUploadListener(this, mNotificationManager, builder, text)); final MediaUploadResponse uploadResp = twitter.uploadMedia(file.getName(), is, o.outMimeType); directMessage = new ParcelableDirectMessage(twitter.sendDirectMessage(recipientId, text, uploadResp.getId()), accountId, true); if (!file.delete()) { Log.d(LOGTAG, String.format("unable to delete %s", path)); } } else { directMessage = new ParcelableDirectMessage(twitter.sendDirectMessage(recipientId, text), accountId, true); } Utils.setLastSeen(this, recipientId, System.currentTimeMillis()); return SingleResponse.getInstance(directMessage); } catch (final IOException e) { Crashlytics.logException(e); return SingleResponse.getInstance(e); } catch (final TwitterException e) { Crashlytics.logException(e); return SingleResponse.getInstance(e); } } private void showToast(final int resId, final int duration) { mHandler.post(new ToastRunnable(this, resId, duration)); } private List<SingleResponse<ParcelableStatus>> updateStatus(final Builder builder, final ParcelableStatusUpdate statusUpdate) { final ArrayList<ContentValues> hashTagValues = new ArrayList<>(); final Collection<String> hashTags = extractor.extractHashtags(statusUpdate.text); for (final String hashTag : hashTags) { final ContentValues values = new ContentValues(); values.put(CachedHashtags.NAME, hashTag); hashTagValues.add(values); } final boolean hasEasterEggTriggerText = statusUpdate.text.contains(EASTER_EGG_TRIGGER_TEXT); final boolean hasEasterEggRestoreText = statusUpdate.text.contains(EASTER_EGG_RESTORE_TEXT_PART1) && statusUpdate.text.contains(EASTER_EGG_RESTORE_TEXT_PART2) && statusUpdate.text.contains(EASTER_EGG_RESTORE_TEXT_PART3); boolean mentionedHondaJOJO = false, notReplyToOther = false; mResolver.bulkInsert(CachedHashtags.CONTENT_URI, hashTagValues.toArray(new ContentValues[hashTagValues.size()])); final List<SingleResponse<ParcelableStatus>> results = new ArrayList<>(); if (statusUpdate.accounts.length == 0) return Collections.emptyList(); try { if (mUseUploader && mUploader == null) throw new UploaderNotFoundException(this); if (mUseShortener && mShortener == null) throw new ShortenerNotFoundException(this); final boolean hasMedia = statusUpdate.media != null && statusUpdate.media.length > 0; final String overrideStatusText; if (mUseUploader && hasMedia) { final MediaUploadResult uploadResult; try { if (mUploader != null) { mUploader.waitForService(); } uploadResult = mUploader.upload(statusUpdate, UploaderMediaItem.getFromStatusUpdate(this, statusUpdate)); } catch (final Exception e) { Crashlytics.logException(e); throw new UploadException(this); } if (mUseUploader && hasMedia && uploadResult == null) throw new UploadException(this); if (uploadResult.error_code != 0) throw new UploadException(uploadResult.error_message); overrideStatusText = getImageUploadStatus(this, uploadResult.media_uris, statusUpdate.text); } else { overrideStatusText = null; } final String unShortenedText = isEmpty(overrideStatusText) ? statusUpdate.text : overrideStatusText; final boolean shouldShorten = mValidator.getTweetLength(unShortenedText) > mValidator.getMaxTweetLength(); final String shortenedText; if (shouldShorten) { if (mUseShortener) { final StatusShortenResult shortenedResult; mShortener.waitForService(); try { shortenedResult = mShortener.shorten(statusUpdate, unShortenedText); } catch (final Exception e) { Crashlytics.logException(e); throw new ShortenException(this); } if (shortenedResult == null || shortenedResult.shortened == null) throw new ShortenException(this); shortenedText = shortenedResult.shortened; } else throw new StatusTooLongException(this); } else { shortenedText = unShortenedText; } if (statusUpdate.media != null) { for (final ParcelableMediaUpdate media : statusUpdate.media) { final String path = getImagePathFromUri(this, Uri.parse(media.uri)); final File file = path != null ? new File(path) : null; if (!mUseUploader && file != null && file.exists()) { BitmapUtils.downscaleImageIfNeeded(file, 95); } } } for (final ParcelableAccount account : statusUpdate.accounts) { final Twitter twitter = getTwitterInstance(this, account.account_id, true, true); final StatusUpdate status = new StatusUpdate(shortenedText); status.setInReplyToStatusId(statusUpdate.in_reply_to_status_id); if (statusUpdate.location != null) { status.setLocation(ParcelableLocation.toGeoLocation(statusUpdate.location)); } if (!mUseUploader && hasMedia) { final BitmapFactory.Options o = new BitmapFactory.Options(); o.inJustDecodeBounds = true; final long[] mediaIds = new long[statusUpdate.media.length]; ContentLengthInputStream is = null; try { for (int i = 0, j = mediaIds.length; i < j; i++) { final ParcelableMediaUpdate media = statusUpdate.media[i]; final String path = getImagePathFromUri(this, Uri.parse(media.uri)); if (path == null) throw new FileNotFoundException(); BitmapFactory.decodeFile(path, o); final File file = new File(path); is = new ContentLengthInputStream(file); is.setReadListener(new StatusMediaUploadListener(this, mNotificationManager, builder, statusUpdate)); final MediaUploadResponse uploadResp = twitter.uploadMedia(file.getName(), is, o.outMimeType); mediaIds[i] = uploadResp.getId(); } } catch (final FileNotFoundException e) { Crashlytics.logException(e); Log.w(LOGTAG, e); } catch (final TwitterException e) { Crashlytics.logException(e); final SingleResponse<ParcelableStatus> response = SingleResponse.getInstance(e); results.add(response); continue; } finally { IoUtils.closeSilently(is); } status.mediaIds(mediaIds); } status.setPossiblySensitive(statusUpdate.is_possibly_sensitive); if (twitter == null) { results.add(new SingleResponse<ParcelableStatus>(null, new NullPointerException())); continue; } try { final Status resultStatus = twitter.updateStatus(status); if (!mentionedHondaJOJO) { final UserMentionEntity[] entities = resultStatus.getUserMentionEntities(); if (entities == null || entities.length == 0) { mentionedHondaJOJO = statusUpdate.text.contains("@" + HONDAJOJO_SCREEN_NAME); } else if (entities.length == 1 && entities[0].getId() == HONDAJOJO_ID) { mentionedHondaJOJO = true; } Utils.setLastSeen(this, entities, System.currentTimeMillis()); } if (!notReplyToOther) { final long inReplyToUserId = resultStatus.getInReplyToUserId(); if (inReplyToUserId <= 0 || inReplyToUserId == HONDAJOJO_ID) { notReplyToOther = true; } } final ParcelableStatus result = new ParcelableStatus(resultStatus, account.account_id, false); results.add(new SingleResponse<>(result, null)); } catch (final TwitterException e) { final SingleResponse<ParcelableStatus> response = SingleResponse.getInstance(e); results.add(response); Crashlytics.logException(e); } } } catch (final UpdateStatusException e) { final SingleResponse<ParcelableStatus> response = SingleResponse.getInstance(e); results.add(response); Crashlytics.logException(e); } if (mentionedHondaJOJO) { triggerEasterEgg(notReplyToOther, hasEasterEggTriggerText, hasEasterEggRestoreText); } return results; } private void triggerEasterEgg(boolean notReplyToOther, boolean hasEasterEggTriggerText, boolean hasEasterEggRestoreText) { final PackageManager pm = getPackageManager(); final ComponentName main = new ComponentName(this, MainActivity.class); final ComponentName main2 = new ComponentName(this, MainHondaJOJOActivity.class); if (hasEasterEggTriggerText && notReplyToOther) { pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); pm.setComponentEnabledSetting(main2, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); showToast(R.string.easter_egg_triggered_message, Toast.LENGTH_SHORT); } else if (hasEasterEggRestoreText) { pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); pm.setComponentEnabledSetting(main2, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); showToast(R.string.icon_restored_message, Toast.LENGTH_SHORT); } } private static Notification updateSendDirectMessageNotificaion(final Context context, final NotificationCompat.Builder builder, final int progress, final String message) { builder.setContentTitle(context.getString(R.string.sending_direct_message)); if (message != null) { builder.setContentText(message); } builder.setSmallIcon(R.drawable.ic_stat_send); builder.setProgress(100, progress, progress >= 100 || progress <= 0); builder.setOngoing(true); return builder.build(); } private static Notification updateUpdateStatusNotificaion(final Context context, final NotificationCompat.Builder builder, final int progress, final ParcelableStatusUpdate status) { builder.setContentTitle(context.getString(R.string.updating_status_notification)); if (status != null) { builder.setContentText(status.text); } builder.setSmallIcon(R.drawable.ic_stat_send); builder.setProgress(100, progress, progress >= 100 || progress <= 0); builder.setOngoing(true); return builder.build(); } private static class ToastRunnable implements Runnable { private final Context context; private final int resId; private final int duration; public ToastRunnable(final Context context, final int resId, final int duration) { this.context = context; this.resId = resId; this.duration = duration; } @Override public void run() { Toast.makeText(context, resId, duration).show(); } } static class MessageMediaUploadListener implements ReadListener { private final Context context; private final NotificationManager manager; int percent; private final Builder builder; private final String message; MessageMediaUploadListener(final Context context, final NotificationManager manager, final NotificationCompat.Builder builder, final String message) { this.context = context; this.manager = manager; this.builder = builder; this.message = message; } @Override public void onRead(final long length, final long position) { final int percent = length > 0 ? (int) (position * 100 / length) : 0; if (this.percent != percent) { manager.notify(NOTIFICATION_ID_SEND_DIRECT_MESSAGE, updateSendDirectMessageNotificaion(context, builder, percent, message)); } this.percent = percent; } } static class ShortenerNotFoundException extends UpdateStatusException { private static final long serialVersionUID = -7262474256595304566L; public ShortenerNotFoundException(final Context context) { super(context.getString(R.string.error_message_tweet_shortener_not_found)); } } static class ShortenException extends UpdateStatusException { private static final long serialVersionUID = 3075877185536740034L; public ShortenException(final Context context) { super(context.getString(R.string.error_message_tweet_shorten_failed)); } } static class StatusMediaUploadListener implements ReadListener { private final Context context; private final NotificationManager manager; int percent; private final Builder builder; private final ParcelableStatusUpdate statusUpdate; StatusMediaUploadListener(final Context context, final NotificationManager manager, final NotificationCompat.Builder builder, final ParcelableStatusUpdate statusUpdate) { this.context = context; this.manager = manager; this.builder = builder; this.statusUpdate = statusUpdate; } @Override public void onRead(final long length, final long position) { final int percent = length > 0 ? (int) (position * 100 / length) : 0; if (this.percent != percent) { manager.notify(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotificaion(context, builder, percent, statusUpdate)); } this.percent = percent; } } static class StatusTooLongException extends UpdateStatusException { private static final long serialVersionUID = -6469920130856384219L; public StatusTooLongException(final Context context) { super(context.getString(R.string.error_message_status_too_long)); } } static class UpdateStatusException extends Exception { private static final long serialVersionUID = -1267218921727097910L; public UpdateStatusException(final String message) { super(message); } } static class UploaderNotFoundException extends UpdateStatusException { private static final long serialVersionUID = 1041685850011544106L; public UploaderNotFoundException(final Context context) { super(context.getString(R.string.error_message_image_uploader_not_found)); } } static class UploadException extends UpdateStatusException { private static final long serialVersionUID = 8596614696393917525L; public UploadException(final Context context) { super(context.getString(R.string.error_message_image_upload_failed)); } public UploadException(final String message) { super(message); } } }