diff --git a/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableActivityExtensions.kt b/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableActivityExtensions.kt index 98c5fbe31..a96423737 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableActivityExtensions.kt +++ b/twidere/src/main/java/org/mariotaku/twidere/model/util/ParcelableActivityExtensions.kt @@ -10,17 +10,20 @@ import org.mariotaku.twidere.model.ParcelableStatus */ fun ParcelableActivity.getActivityStatus(): ParcelableStatus? { val status: ParcelableStatus - if (Activity.Action.MENTION == action) { - if (ArrayUtils.isEmpty(target_object_statuses)) return null - status = target_object_statuses[0] - } else if (Activity.Action.REPLY == action) { - if (ArrayUtils.isEmpty(target_statuses)) return null - status = target_statuses[0] - } else if (Activity.Action.QUOTE == action) { - if (ArrayUtils.isEmpty(target_statuses)) return null - status = target_statuses[0] - } else { - return null + when (action) { + Activity.Action.MENTION -> { + if (ArrayUtils.isEmpty(target_object_statuses)) return null + status = target_object_statuses[0] + } + Activity.Action.REPLY -> { + if (ArrayUtils.isEmpty(target_statuses)) return null + status = target_statuses[0] + } + Activity.Action.QUOTE -> { + if (ArrayUtils.isEmpty(target_statuses)) return null + status = target_statuses[0] + } + else -> return null } status.account_color = account_color status.user_color = status_user_color diff --git a/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java b/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java deleted file mode 100644 index e12f61a6c..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java +++ /dev/null @@ -1,615 +0,0 @@ -/* - * Twidere - Twitter client for Android - * - * Copyright (C) 2012-2014 Mariotaku Lee - * - * 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 . - */ - -package org.mariotaku.twidere.service; - -import android.app.IntentService; -import android.app.Notification; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; -import android.os.Parcelable; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.WorkerThread; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.NotificationCompat.Builder; -import android.text.TextUtils; -import android.util.Log; -import android.util.Pair; -import android.widget.Toast; - -import com.twitter.Extractor; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.mariotaku.abstask.library.ManualTaskStarter; -import org.mariotaku.microblog.library.MicroBlog; -import org.mariotaku.microblog.library.MicroBlogException; -import org.mariotaku.microblog.library.twitter.TwitterUpload; -import org.mariotaku.microblog.library.twitter.model.DirectMessage; -import org.mariotaku.microblog.library.twitter.model.ErrorInfo; -import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse; -import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse.ProcessingInfo; -import org.mariotaku.restfu.http.ContentType; -import org.mariotaku.restfu.http.mime.Body; -import org.mariotaku.restfu.http.mime.FileBody; -import org.mariotaku.restfu.http.mime.SimpleBody; -import org.mariotaku.sqliteqb.library.Expression; -import org.mariotaku.twidere.Constants; -import org.mariotaku.twidere.R; -import org.mariotaku.twidere.model.Draft; -import org.mariotaku.twidere.model.DraftCursorIndices; -import org.mariotaku.twidere.model.ParcelableAccount; -import org.mariotaku.twidere.model.ParcelableCredentials; -import org.mariotaku.twidere.model.ParcelableDirectMessage; -import org.mariotaku.twidere.model.ParcelableStatus; -import org.mariotaku.twidere.model.ParcelableStatusUpdate; -import org.mariotaku.twidere.model.SingleResponse; -import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra; -import org.mariotaku.twidere.model.util.ParcelableAccountUtils; -import org.mariotaku.twidere.model.util.ParcelableCredentialsUtils; -import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils; -import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils; -import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages; -import org.mariotaku.twidere.provider.TwidereDataStore.Drafts; -import org.mariotaku.twidere.task.twitter.UpdateStatusTask; -import org.mariotaku.twidere.util.AsyncTwitterWrapper; -import org.mariotaku.twidere.util.ContentValuesCreator; -import org.mariotaku.twidere.util.MicroBlogAPIFactory; -import org.mariotaku.twidere.util.NotificationManagerWrapper; -import org.mariotaku.twidere.util.SharedPreferencesWrapper; -import org.mariotaku.twidere.util.TwidereValidator; -import org.mariotaku.twidere.util.Utils; -import org.mariotaku.twidere.util.dagger.GeneralComponentHelper; -import org.mariotaku.twidere.util.io.ContentLengthInputStream.ReadListener; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -import edu.tsinghua.hotmobi.HotMobiLogger; -import edu.tsinghua.hotmobi.model.TimelineType; -import edu.tsinghua.hotmobi.model.TweetEvent; - -public class BackgroundOperationService extends IntentService implements Constants { - - - private Handler mHandler; - @Inject - SharedPreferencesWrapper mPreferences; - @Inject - AsyncTwitterWrapper mTwitter; - @Inject - NotificationManagerWrapper mNotificationManager; - @Inject - TwidereValidator mValidator; - @Inject - Extractor mExtractor; - private static final long BULK_SIZE = 128 * 1024; // 128KiB - - - public BackgroundOperationService() { - super("background_operation"); - } - - - @Override - public void onCreate() { - super.onCreate(); - GeneralComponentHelper.build(this).inject(this); - mHandler = new Handler(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - } - - @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 longMessage) { - mHandler.post(new Runnable() { - - @Override - public void run() { - Utils.showErrorMessage(BackgroundOperationService.this, message, longMessage); - } - }); - } - - public void showErrorMessage(final int actionRes, final Exception e, final boolean longMessage) { - mHandler.post(new Runnable() { - - @Override - public void run() { - Utils.showErrorMessage(BackgroundOperationService.this, actionRes, e, longMessage); - } - }); - } - - public void showErrorMessage(final int actionRes, final String message, final boolean longMessage) { - mHandler.post(new Runnable() { - - @Override - public void run() { - Utils.showErrorMessage(BackgroundOperationService.this, actionRes, message, longMessage); - } - }); - } - - public void showOkMessage(final int messageRes, final boolean longMessage) { - showToast(getString(messageRes), longMessage); - } - - private void showToast(final CharSequence message, final boolean longMessage) { - mHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(BackgroundOperationService.this, message, longMessage ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show(); - } - }); - } - - @Override - protected void onHandleIntent(final Intent intent) { - if (intent == null) return; - final String action = intent.getAction(); - if (action == null) return; - 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; - case INTENT_ACTION_SEND_DRAFT: { - handleSendDraftIntent(intent); - } - } - } - - private void handleSendDraftIntent(Intent intent) { - final Uri uri = intent.getData(); - if (uri == null) return; - mNotificationManager.cancel(uri.toString(), NOTIFICATION_ID_DRAFTS); - final long def = -1; - final long draftId = NumberUtils.toLong(uri.getLastPathSegment(), def); - if (draftId == -1) return; - final Expression where = Expression.equals(Drafts._ID, draftId); - final ContentResolver cr = getContentResolver(); - final Cursor c = cr.query(Drafts.CONTENT_URI, Drafts.COLUMNS, where.getSQL(), null, null); - if (c == null) return; - final DraftCursorIndices i = new DraftCursorIndices(c); - final Draft item; - try { - if (!c.moveToFirst()) return; - item = i.newObject(c); - } finally { - c.close(); - } - cr.delete(Drafts.CONTENT_URI, where.getSQL(), null); - if (TextUtils.isEmpty(item.action_type)) { - item.action_type = Draft.Action.UPDATE_STATUS; - } - switch (item.action_type) { - case Draft.Action.UPDATE_STATUS_COMPAT_1: - case Draft.Action.UPDATE_STATUS_COMPAT_2: - case Draft.Action.UPDATE_STATUS: - case Draft.Action.REPLY: - case Draft.Action.QUOTE: { - updateStatuses(item.action_type, ParcelableStatusUpdateUtils.fromDraftItem(this, item)); - break; - } - case Draft.Action.SEND_DIRECT_MESSAGE_COMPAT: - case Draft.Action.SEND_DIRECT_MESSAGE: { - String recipientId = null; - if (item.action_extras instanceof SendDirectMessageActionExtra) { - recipientId = ((SendDirectMessageActionExtra) item.action_extras).getRecipientId(); - } - if (ArrayUtils.isEmpty(item.account_keys) || recipientId == null) { - return; - } - final UserKey accountKey = item.account_keys[0]; - final String imageUri = ArrayUtils.isEmpty(item.media) ? null : item.media[0].uri; - sendMessage(accountKey, recipientId, item.text, imageUri); - break; - } - } - } - - private void handleDiscardDraftIntent(Intent intent) { - final Uri data = intent.getData(); - if (data == null) return; - mNotificationManager.cancel(data.toString(), NOTIFICATION_ID_DRAFTS); - final ContentResolver cr = getContentResolver(); - final long def = -1; - final long id = NumberUtils.toLong(data.getLastPathSegment(), def); - final Expression where = Expression.equals(Drafts._ID, id); - cr.delete(Drafts.CONTENT_URI, where.getSQL(), null); - } - - private void handleSendDirectMessageIntent(final Intent intent) { - final UserKey accountId = intent.getParcelableExtra(EXTRA_ACCOUNT_KEY); - final String recipientId = intent.getStringExtra(EXTRA_RECIPIENT_ID); - final String text = intent.getStringExtra(EXTRA_TEXT); - final String imageUri = intent.getStringExtra(EXTRA_IMAGE_URI); - if (accountId == null || recipientId == null || text == null) return; - sendMessage(accountId, recipientId, text, imageUri); - } - - private void sendMessage(@NonNull UserKey accountId, @NonNull String recipientId, - @NonNull String text, @Nullable String imageUri) { - final String title = getString(R.string.sending_direct_message); - final Builder builder = new Builder(this); - builder.setSmallIcon(R.drawable.ic_stat_send); - builder.setProgress(100, 0, true); - builder.setTicker(title); - builder.setContentTitle(title); - builder.setContentText(text); - builder.setCategory(NotificationCompat.CATEGORY_PROGRESS); - builder.setOngoing(true); - final Notification notification = builder.build(); - startForeground(NOTIFICATION_ID_SEND_DIRECT_MESSAGE, notification); - final SingleResponse result = sendDirectMessage(builder, accountId, - recipientId, text, imageUri); - - final ContentResolver resolver = getContentResolver(); - if (result.hasData()) { - final ParcelableDirectMessage message = result.getData(); - final ContentValues values = ContentValuesCreator.createDirectMessage(message); - final String deleteWhere = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY), - Expression.equalsArgs(DirectMessages.MESSAGE_ID)).getSQL(); - String[] deleteWhereArgs = {message.account_key.toString(), message.id}; - resolver.delete(DirectMessages.Outbox.CONTENT_URI, deleteWhere, deleteWhereArgs); - resolver.insert(DirectMessages.Outbox.CONTENT_URI, values); - showOkMessage(R.string.direct_message_sent, false); - } else { - final ContentValues values = ContentValuesCreator.createMessageDraft(accountId, recipientId, text, imageUri); - resolver.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 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; - @Draft.Action - final String actionType = intent.getStringExtra(EXTRA_ACTION); - updateStatuses(actionType, statuses); - } - - private void updateStatuses(@Draft.Action final String actionType, final ParcelableStatusUpdate... statuses) { - final BackgroundOperationService context = this; - final Builder builder = new Builder(context); - startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, - builder, 0, null)); - for (final ParcelableStatusUpdate item : statuses) { - final UpdateStatusTask task = new UpdateStatusTask(context, new UpdateStatusTask.StateCallback() { - - @WorkerThread - @Override - public void onStartUploadingMedia() { - startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, - builder, 0, item)); - } - - @WorkerThread - @Override - public void onUploadingProgressChanged(int index, long current, long total) { - int progress = (int) (current * 100 / total); - startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, - builder, progress, item)); - } - - @WorkerThread - @Override - public void onShorteningStatus() { - startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, - builder, 0, item)); - } - - @WorkerThread - @Override - public void onUpdatingStatus() { - startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, - builder, 0, item)); - } - - @Override - public void afterExecute(Context handler, UpdateStatusTask.UpdateStatusResult result) { - boolean failed = false; - final UpdateStatusTask.UpdateStatusException exception = result.getException(); - final MicroBlogException[] exceptions = result.getExceptions(); - if (exception != null) { - Toast.makeText(context, exception.getMessage(), Toast.LENGTH_SHORT).show(); - failed = true; - Log.w(LOGTAG, exception); - } else for (MicroBlogException e : exceptions) { - if (e != null) { - // Show error - String errorMessage = Utils.getErrorMessage(context, e); - if (TextUtils.isEmpty(errorMessage)) { - errorMessage = context.getString(R.string.status_not_updated); - } - Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show(); - failed = true; - break; - } - } - if (failed) { - // TODO show draft notification - } else { - Toast.makeText(context, R.string.status_updated, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void beforeExecute() { - - } - }); - task.setCallback(this); - task.setParams(Pair.create(actionType, item)); - mHandler.post(new Runnable() { - @Override - public void run() { - ManualTaskStarter.invokeBeforeExecute(task); - } - }); - - final UpdateStatusTask.UpdateStatusResult result = ManualTaskStarter.invokeExecute(task); - mHandler.post(new Runnable() { - @Override - public void run() { - ManualTaskStarter.invokeAfterExecute(task, result); - } - }); - - final UpdateStatusTask.UpdateStatusException exception = result.getException(); - final ParcelableStatus[] updatedStatuses = result.getStatuses(); - if (exception != null) { - Log.w(LOGTAG, exception); - } else for (ParcelableStatus status : updatedStatuses) { - if (status == null) continue; - final TweetEvent event = TweetEvent.create(context, status, TimelineType.OTHER); - event.setAction(TweetEvent.Action.TWEET); - HotMobiLogger.getInstance(context).log(status.account_key, event); - } - } - if (mPreferences.getBoolean(KEY_REFRESH_AFTER_TWEET)) { - mHandler.post(new Runnable() { - @Override - public void run() { - mTwitter.refreshAll(); - } - }); - } - stopForeground(false); - mNotificationManager.cancel(NOTIFICATION_ID_UPDATE_STATUS); - } - - - private SingleResponse sendDirectMessage(final NotificationCompat.Builder builder, - final UserKey accountKey, - final String recipientId, - final String text, - final String imageUri) { - final ParcelableCredentials credentials = ParcelableCredentialsUtils.getCredentials(this, - accountKey); - if (credentials == null) return SingleResponse.Companion.getInstance(); - final MicroBlog twitter = MicroBlogAPIFactory.getInstance(this, credentials, true, true); - final TwitterUpload twitterUpload = MicroBlogAPIFactory.getInstance(this, credentials, - true, true, TwitterUpload.class); - if (twitter == null || twitterUpload == null) return SingleResponse.Companion.getInstance(); - try { - final ParcelableDirectMessage directMessage; - switch (ParcelableAccountUtils.getAccountType(credentials)) { - case ParcelableAccount.Type.FANFOU: { - if (imageUri != null) { - throw new MicroBlogException("Can't send image DM on Fanfou"); - } - final DirectMessage dm = twitter.sendFanfouDirectMessage(recipientId, text); - directMessage = ParcelableDirectMessageUtils.fromDirectMessage(dm, accountKey, true); - break; - } - default: { - if (imageUri != null) { - final Uri mediaUri = Uri.parse(imageUri); - FileBody body = null; - try { - body = UpdateStatusTask.Companion.getBodyFromMedia(getContentResolver(), mediaUri, - new MessageMediaUploadListener(this, mNotificationManager, - builder, text), null); - final MediaUploadResponse uploadResp = uploadMedia(twitterUpload, body); - final DirectMessage response = twitter.sendDirectMessage(recipientId, - text, uploadResp.getId()); - directMessage = ParcelableDirectMessageUtils.fromDirectMessage(response, - accountKey, true); - } finally { - Utils.closeSilently(body); - } - final String path = Utils.getImagePathFromUri(this, mediaUri); - if (path != null) { - final File file = new File(path); - if (!file.delete()) { - Log.d(LOGTAG, String.format("unable to delete %s", path)); - } - } - } else { - final DirectMessage response = twitter.sendDirectMessage(recipientId, text); - directMessage = ParcelableDirectMessageUtils.fromDirectMessage(response, - accountKey, true); - } - break; - } - } - Utils.setLastSeen(this, new UserKey(recipientId, accountKey.getHost()), - System.currentTimeMillis()); - - return SingleResponse.Companion.getInstance(directMessage); - } catch (final IOException e) { - return SingleResponse.Companion.getInstance(e); - } catch (final MicroBlogException e) { - return SingleResponse.Companion.getInstance(e); - } - } - - - private MediaUploadResponse uploadMedia(final TwitterUpload upload, Body body) throws IOException, MicroBlogException { - final String mediaType = body.contentType().getContentType(); - final long length = body.length(); - final InputStream stream = body.stream(); - MediaUploadResponse response = upload.initUploadMedia(mediaType, length, null); - final int segments = length == 0 ? 0 : (int) (length / BULK_SIZE + 1); - for (int segmentIndex = 0; segmentIndex < segments; segmentIndex++) { - final int currentBulkSize = (int) Math.min(BULK_SIZE, length - segmentIndex * BULK_SIZE); - final SimpleBody bulk = new SimpleBody(ContentType.OCTET_STREAM, null, currentBulkSize, - stream); - upload.appendUploadMedia(response.getId(), segmentIndex, bulk); - } - response = upload.finalizeUploadMedia(response.getId()); - for (ProcessingInfo info = response.getProcessingInfo(); shouldWaitForProcess(info); info = response.getProcessingInfo()) { - final long checkAfterSecs = info.getCheckAfterSecs(); - if (checkAfterSecs <= 0) { - break; - } - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(checkAfterSecs)); - } catch (InterruptedException e) { - break; - } - response = upload.getUploadMediaStatus(response.getId()); - } - ProcessingInfo info = response.getProcessingInfo(); - if (info != null && ProcessingInfo.State.FAILED.equals(info.getState())) { - final MicroBlogException exception = new MicroBlogException(); - ErrorInfo errorInfo = info.getError(); - if (errorInfo != null) { - exception.setErrors(new ErrorInfo[]{errorInfo}); - } - throw exception; - } - return response; - } - - private boolean shouldWaitForProcess(ProcessingInfo info) { - if (info == null) return false; - switch (info.getState()) { - case ProcessingInfo.State.PENDING: - case ProcessingInfo.State.IN_PROGRESS: - return true; - default: - return false; - } - } - - private static Notification updateSendDirectMessageNotification(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 updateUpdateStatusNotification(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(); - } - - public static void updateStatusesAsync(Context context, @Draft.Action final String action, - final ParcelableStatusUpdate... statuses) { - final Intent intent = new Intent(context, BackgroundOperationService.class); - intent.setAction(INTENT_ACTION_UPDATE_STATUS); - intent.putExtra(EXTRA_STATUSES, statuses); - intent.putExtra(EXTRA_ACTION, action); - context.startService(intent); - } - - static class MessageMediaUploadListener implements ReadListener { - private final Context context; - private final NotificationManagerWrapper manager; - - int percent; - - private final Builder builder; - private final String message; - - MessageMediaUploadListener(final Context context, final NotificationManagerWrapper 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, - updateSendDirectMessageNotification(context, builder, percent, message)); - } - this.percent = percent; - } - } - -} diff --git a/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt b/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt index 162b8038f..7c71f33dd 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt +++ b/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt @@ -5,12 +5,12 @@ import android.content.ContentValues import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.Point import android.net.Uri import android.support.annotation.UiThread import android.support.annotation.WorkerThread import android.text.TextUtils import android.util.Pair -import android.util.Size import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.math.NumberUtils import org.mariotaku.abstask.library.AbstractTask @@ -228,8 +228,13 @@ class UpdateStatusTask(internal val context: Context, internal val stateCallback result.exceptions[i] = MicroBlogException( context.getString(R.string.error_too_many_photos_fanfou)) } else { + val sizeLimit = Point(2048, 1536) body = getBodyFromMedia(context.contentResolver, - Uri.parse(statusUpdate.media[0].uri), ContentLengthInputStream.ReadListener { length, position -> stateCallback.onUploadingProgressChanged(-1, position, length) }) + Uri.parse(statusUpdate.media[0].uri), + sizeLimit, + ContentLengthInputStream.ReadListener { length, position -> + stateCallback.onUploadingProgressChanged(-1, position, length) + }) val photoUpdate = PhotoStatusUpdate(body, pendingUpdate.overrideTexts[i]) val requestResult = microBlog.uploadPhoto(photoUpdate) @@ -413,9 +418,11 @@ class UpdateStatusTask(internal val context: Context, internal val stateCallback //noinspection TryWithIdenticalCatches var body: Body? = null try { - body = getBodyFromMedia(context.contentResolver, Uri.parse(media.uri), ContentLengthInputStream.ReadListener { length, position -> - stateCallback.onUploadingProgressChanged(index, position, length) - }) + val sizeLimit = Point(2048, 1536) + body = getBodyFromMedia(context.contentResolver, Uri.parse(media.uri), sizeLimit, + ContentLengthInputStream.ReadListener { length, position -> + stateCallback.onUploadingProgressChanged(index, position, length) + }) if (chucked) { resp = uploadMediaChucked(upload, body, ownerIds) } else { @@ -652,8 +659,8 @@ class UpdateStatusTask(internal val context: Context, internal val stateCallback @Throws(IOException::class) fun getBodyFromMedia(resolver: ContentResolver, mediaUri: Uri, - readListener: ContentLengthInputStream.ReadListener, - sizeLimit: Size? = null): FileBody { + sizeLimit: Point? = null, + readListener: ContentLengthInputStream.ReadListener): FileBody { val mediaType = resolver.getType(mediaUri) val st = resolver.openInputStream(mediaUri) ?: throw FileNotFoundException(mediaUri.toString()) val cis: ContentLengthInputStream @@ -662,6 +669,8 @@ class UpdateStatusTask(internal val context: Context, internal val stateCallback val o = BitmapFactory.Options() o.inJustDecodeBounds = true BitmapFactory.decodeStream(st, null, o) + o.inSampleSize = Utils.calculateInSampleSize(o.outWidth, o.outHeight, + sizeLimit.x, sizeLimit.y) o.inJustDecodeBounds = false val bitmap = BitmapFactory.decodeStream(st, null, o) val os = DirectByteArrayOutputStream() diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/ContentValuesCreator.java b/twidere/src/main/java/org/mariotaku/twidere/util/ContentValuesCreator.java index 25d9fa835..795b2c145 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/ContentValuesCreator.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/ContentValuesCreator.java @@ -164,7 +164,8 @@ public final class ContentValuesCreator implements TwidereConstants { @NonNull public static ContentValues createActivity(final ParcelableActivity activity, - ParcelableCredentials credentials, UserColorNameManager manager) { + final ParcelableCredentials credentials, + final UserColorNameManager manager) { final ContentValues values = new ContentValues(); final ParcelableStatus status = ParcelableActivityExtensionsKt.getActivityStatus(activity); diff --git a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java index 5ce9e11f3..72df71fc4 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java +++ b/twidere/src/main/java/org/mariotaku/twidere/util/Utils.java @@ -1155,7 +1155,7 @@ public final class Utils implements Constants { return context.getString(R.string.error_message_with_action, action, message); } - public static String getErrorMessage(final Context context, final CharSequence action, final Throwable t) { + public static String getErrorMessage(final Context context, final CharSequence action, @Nullable final Throwable t) { if (context == null) return null; if (t instanceof MicroBlogException) return getTwitterErrorMessage(context, action, (MicroBlogException) t); @@ -1786,7 +1786,7 @@ public final class Utils implements Constants { } public static void showErrorMessage(final Context context, final CharSequence action, - final Throwable t, final boolean longMessage) { + @Nullable final Throwable t, final boolean longMessage) { if (context == null) return; if (t instanceof MicroBlogException) { showTwitterErrorMessage(context, action, (MicroBlogException) t, longMessage); @@ -1801,7 +1801,8 @@ public final class Utils implements Constants { showErrorMessage(context, context.getString(actionRes), desc, longMessage); } - public static void showErrorMessage(final Context context, final int action, final Throwable t, + public static void showErrorMessage(final Context context, final int action, + @Nullable final Throwable t, final boolean long_message) { if (context == null) return; showErrorMessage(context, context.getString(action), t, long_message); diff --git a/twidere/src/main/kotlin/org/mariotaku/ktextension/NumberExtensions.kt b/twidere/src/main/kotlin/org/mariotaku/ktextension/NumberExtensions.kt new file mode 100644 index 000000000..53e2c9412 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/ktextension/NumberExtensions.kt @@ -0,0 +1,13 @@ +package org.mariotaku.ktextension + +/** + * Created by mariotaku on 16/7/30. + */ + +fun String.toLong(def: Long): Long { + try { + return toLong() + } catch (e: NumberFormatException) { + return def + } +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt index 1404f67aa..cb431290c 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/adapter/ParcelableActivitiesAdapter.kt @@ -283,7 +283,12 @@ class ParcelableActivitiesAdapter( if (followingOnly && !activity.status_user_following) return ITEM_VIEW_TYPE_EMPTY return ITEM_VIEW_TYPE_STATUS } - Activity.Action.FOLLOW, Activity.Action.FAVORITE, Activity.Action.RETWEET, Activity.Action.FAVORITED_RETWEET, Activity.Action.RETWEETED_RETWEET, Activity.Action.RETWEETED_MENTION, Activity.Action.FAVORITED_MENTION, Activity.Action.LIST_CREATED, Activity.Action.LIST_MEMBER_ADDED, Activity.Action.MEDIA_TAGGED, Activity.Action.RETWEETED_MEDIA_TAGGED, Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> { + Activity.Action.FOLLOW, Activity.Action.FAVORITE, Activity.Action.RETWEET, + Activity.Action.FAVORITED_RETWEET, Activity.Action.RETWEETED_RETWEET, + Activity.Action.RETWEETED_MENTION, Activity.Action.FAVORITED_MENTION, + Activity.Action.LIST_CREATED, Activity.Action.LIST_MEMBER_ADDED, + Activity.Action.MEDIA_TAGGED, Activity.Action.RETWEETED_MEDIA_TAGGED, + Activity.Action.FAVORITED_MEDIA_TAGGED, Activity.Action.JOINED_TWITTER -> { if (mentionsOnly) return ITEM_VIEW_TYPE_EMPTY ParcelableActivityUtils.initAfterFilteredSourceIds(activity, filteredUserIds, followingOnly) if (ArrayUtils.isEmpty(activity.after_filtered_source_ids)) { diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt new file mode 100644 index 000000000..f032f3773 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/service/BackgroundOperationService.kt @@ -0,0 +1,518 @@ +/* + * Twidere - Twitter client for Android + * + * Copyright (C) 2012-2014 Mariotaku Lee + * + * 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 . + */ + +package org.mariotaku.twidere.service + +import android.app.IntentService +import android.app.Notification +import android.app.Service +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Handler +import android.support.annotation.WorkerThread +import android.support.v4.app.NotificationCompat +import android.support.v4.app.NotificationCompat.Builder +import android.text.TextUtils +import android.util.Log +import android.util.Pair +import android.widget.Toast +import com.twitter.Extractor +import edu.tsinghua.hotmobi.HotMobiLogger +import edu.tsinghua.hotmobi.model.TimelineType +import edu.tsinghua.hotmobi.model.TweetEvent +import org.apache.commons.lang3.ArrayUtils +import org.apache.commons.lang3.math.NumberUtils +import org.mariotaku.abstask.library.ManualTaskStarter +import org.mariotaku.ktextension.asTypedArray +import org.mariotaku.ktextension.toLong +import org.mariotaku.microblog.library.MicroBlogException +import org.mariotaku.microblog.library.twitter.TwitterUpload +import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse +import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse.ProcessingInfo +import org.mariotaku.restfu.http.ContentType +import org.mariotaku.restfu.http.mime.Body +import org.mariotaku.restfu.http.mime.FileBody +import org.mariotaku.restfu.http.mime.SimpleBody +import org.mariotaku.sqliteqb.library.Expression +import org.mariotaku.twidere.Constants +import org.mariotaku.twidere.R +import org.mariotaku.twidere.TwidereConstants.* +import org.mariotaku.twidere.model.* +import org.mariotaku.twidere.model.draft.SendDirectMessageActionExtra +import org.mariotaku.twidere.model.util.ParcelableAccountUtils +import org.mariotaku.twidere.model.util.ParcelableCredentialsUtils +import org.mariotaku.twidere.model.util.ParcelableDirectMessageUtils +import org.mariotaku.twidere.model.util.ParcelableStatusUpdateUtils +import org.mariotaku.twidere.provider.TwidereDataStore.DirectMessages +import org.mariotaku.twidere.provider.TwidereDataStore.Drafts +import org.mariotaku.twidere.task.twitter.UpdateStatusTask +import org.mariotaku.twidere.util.* +import org.mariotaku.twidere.util.dagger.GeneralComponentHelper +import org.mariotaku.twidere.util.io.ContentLengthInputStream.ReadListener +import java.io.File +import java.io.IOException +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BackgroundOperationService : IntentService("background_operation"), Constants { + + + private var handler: Handler? = null + @Inject + lateinit var preferences: SharedPreferencesWrapper + @Inject + lateinit var twitterWrapper: AsyncTwitterWrapper + @Inject + lateinit var notificationManager: NotificationManagerWrapper + @Inject + lateinit var validator: TwidereValidator + @Inject + lateinit var extractor: Extractor + + + override fun onCreate() { + super.onCreate() + GeneralComponentHelper.build(this).inject(this) + handler = Handler() + } + + override fun onDestroy() { + super.onDestroy() + } + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + return Service.START_STICKY + } + + fun showErrorMessage(message: CharSequence, longMessage: Boolean) { + handler!!.post { Utils.showErrorMessage(this@BackgroundOperationService, message, longMessage) } + } + + fun showErrorMessage(actionRes: Int, e: Exception?, longMessage: Boolean) { + handler!!.post { Utils.showErrorMessage(this@BackgroundOperationService, actionRes, e, longMessage) } + } + + fun showErrorMessage(actionRes: Int, message: String, longMessage: Boolean) { + handler!!.post { Utils.showErrorMessage(this@BackgroundOperationService, actionRes, message, longMessage) } + } + + fun showOkMessage(messageRes: Int, longMessage: Boolean) { + showToast(getString(messageRes), longMessage) + } + + private fun showToast(message: CharSequence, longMessage: Boolean) { + handler!!.post { Toast.makeText(this@BackgroundOperationService, message, if (longMessage) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() } + } + + override fun onHandleIntent(intent: Intent?) { + if (intent == null) return + val action = intent.action ?: return + when (action) { + INTENT_ACTION_UPDATE_STATUS -> { + handleUpdateStatusIntent(intent) + } + INTENT_ACTION_SEND_DIRECT_MESSAGE -> { + handleSendDirectMessageIntent(intent) + } + INTENT_ACTION_DISCARD_DRAFT -> { + handleDiscardDraftIntent(intent) + } + INTENT_ACTION_SEND_DRAFT -> { + handleSendDraftIntent(intent) + } + } + } + + private fun handleSendDraftIntent(intent: Intent) { + val uri = intent.data ?: return + notificationManager.cancel(uri.toString(), NOTIFICATION_ID_DRAFTS) + val draftId = uri.lastPathSegment.toLong(-1) + if (draftId == -1L) return + val where = Expression.equals(Drafts._ID, draftId) + val cr = contentResolver + val c = cr.query(Drafts.CONTENT_URI, Drafts.COLUMNS, where.sql, null, null) ?: return + val i = DraftCursorIndices(c) + val item: Draft + try { + if (!c.moveToFirst()) return + item = i.newObject(c) + } finally { + c.close() + } + cr.delete(Drafts.CONTENT_URI, where.sql, null) + if (TextUtils.isEmpty(item.action_type)) { + item.action_type = Draft.Action.UPDATE_STATUS + } + when (item.action_type) { + Draft.Action.UPDATE_STATUS_COMPAT_1, Draft.Action.UPDATE_STATUS_COMPAT_2, Draft.Action.UPDATE_STATUS, Draft.Action.REPLY, Draft.Action.QUOTE -> { + updateStatuses(item.action_type, ParcelableStatusUpdateUtils.fromDraftItem(this, item)) + } + Draft.Action.SEND_DIRECT_MESSAGE_COMPAT, Draft.Action.SEND_DIRECT_MESSAGE -> { + var recipientId: String? = null + if (item.action_extras is SendDirectMessageActionExtra) { + recipientId = (item.action_extras as SendDirectMessageActionExtra).recipientId + } + if (ArrayUtils.isEmpty(item.account_keys) || recipientId == null) { + return + } + val accountKey = item.account_keys!![0] + val imageUri = if (ArrayUtils.isEmpty(item.media)) null else item.media[0].uri + sendMessage(accountKey, recipientId, item.text, imageUri) + } + } + } + + private fun handleDiscardDraftIntent(intent: Intent) { + val data = intent.data ?: return + notificationManager.cancel(data.toString(), NOTIFICATION_ID_DRAFTS) + val cr = contentResolver + val def: Long = -1 + val id = NumberUtils.toLong(data.lastPathSegment, def) + val where = Expression.equals(Drafts._ID, id) + cr.delete(Drafts.CONTENT_URI, where.sql, null) + } + + private fun handleSendDirectMessageIntent(intent: Intent) { + val accountId = intent.getParcelableExtra(EXTRA_ACCOUNT_KEY) + val recipientId = intent.getStringExtra(EXTRA_RECIPIENT_ID) + val text = intent.getStringExtra(EXTRA_TEXT) + val imageUri = intent.getStringExtra(EXTRA_IMAGE_URI) + if (accountId == null || recipientId == null || text == null) return + sendMessage(accountId, recipientId, text, imageUri) + } + + private fun sendMessage(accountId: UserKey, recipientId: String, + text: String, imageUri: String?) { + val title = getString(R.string.sending_direct_message) + val builder = Builder(this) + builder.setSmallIcon(R.drawable.ic_stat_send) + builder.setProgress(100, 0, true) + builder.setTicker(title) + builder.setContentTitle(title) + builder.setContentText(text) + builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) + builder.setOngoing(true) + val notification = builder.build() + startForeground(NOTIFICATION_ID_SEND_DIRECT_MESSAGE, notification) + val result = sendDirectMessage(builder, accountId, + recipientId, text, imageUri) + + val resolver = contentResolver + if (result.hasData()) { + val message = result.data + val values = ContentValuesCreator.createDirectMessage(message) + val deleteWhere = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY), + Expression.equalsArgs(DirectMessages.MESSAGE_ID)).sql + val deleteWhereArgs = arrayOf(message!!.account_key.toString(), message.id) + resolver.delete(DirectMessages.Outbox.CONTENT_URI, deleteWhere, deleteWhereArgs) + resolver.insert(DirectMessages.Outbox.CONTENT_URI, values) + showOkMessage(R.string.direct_message_sent, false) + } else { + val values = ContentValuesCreator.createMessageDraft(accountId, recipientId, text, imageUri) + resolver.insert(Drafts.CONTENT_URI, values) + showErrorMessage(R.string.action_sending_direct_message, result.exception, true) + } + stopForeground(false) + notificationManager.cancel(NOTIFICATION_ID_SEND_DIRECT_MESSAGE) + } + + private fun handleUpdateStatusIntent(intent: Intent) { + val status = intent.getParcelableExtra(EXTRA_STATUS) + val statusParcelables = intent.getParcelableArrayExtra(EXTRA_STATUSES) + val statuses: Array + if (statusParcelables != null) { + statuses = statusParcelables.asTypedArray(ParcelableStatusUpdate.CREATOR) + } else if (status != null) { + statuses = arrayOf(status) + } else + return + @Draft.Action + val actionType = intent.getStringExtra(EXTRA_ACTION) + updateStatuses(actionType, *statuses) + } + + private fun updateStatuses(@Draft.Action actionType: String, vararg statuses: ParcelableStatusUpdate) { + val context = this + val builder = Builder(context) + startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, + builder, 0, null)) + for (item in statuses) { + val task = UpdateStatusTask(context, object : UpdateStatusTask.StateCallback { + + @WorkerThread + override fun onStartUploadingMedia() { + startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, + builder, 0, item)) + } + + @WorkerThread + override fun onUploadingProgressChanged(index: Int, current: Long, total: Long) { + val progress = (current * 100 / total).toInt() + startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, + builder, progress, item)) + } + + @WorkerThread + override fun onShorteningStatus() { + startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, + builder, 0, item)) + } + + @WorkerThread + override fun onUpdatingStatus() { + startForeground(NOTIFICATION_ID_UPDATE_STATUS, updateUpdateStatusNotification(context, + builder, 0, item)) + } + + override fun afterExecute(handler: Context?, result: UpdateStatusTask.UpdateStatusResult?) { + var failed = false + val exception = result!!.exception + val exceptions = result.exceptions + if (exception != null) { + Toast.makeText(context, exception.message, Toast.LENGTH_SHORT).show() + failed = true + Log.w(LOGTAG, exception) + } else for (e in exceptions) { + if (e != null) { + // Show error + var errorMessage = Utils.getErrorMessage(context, e) + if (TextUtils.isEmpty(errorMessage)) { + errorMessage = context.getString(R.string.status_not_updated) + } + Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show() + failed = true + break + } + } + if (failed) { + // TODO show draft notification + } else { + Toast.makeText(context, R.string.status_updated, Toast.LENGTH_SHORT).show() + } + } + + override fun beforeExecute() { + + } + }) + task.setCallback(this) + task.params = Pair.create(actionType, item) + handler!!.post { ManualTaskStarter.invokeBeforeExecute(task) } + + val result = ManualTaskStarter.invokeExecute(task) + handler!!.post { ManualTaskStarter.invokeAfterExecute(task, result) } + + val exception = result.exception + val updatedStatuses = result.statuses + if (exception != null) { + Log.w(LOGTAG, exception) + } else + for (status in updatedStatuses) { + if (status == null) continue + val event = TweetEvent.create(context, status, TimelineType.OTHER) + event.setAction(TweetEvent.Action.TWEET) + HotMobiLogger.getInstance(context).log(status.account_key, event) + } + } + if (preferences.getBoolean(KEY_REFRESH_AFTER_TWEET)) { + handler!!.post { twitterWrapper.refreshAll() } + } + stopForeground(false) + notificationManager.cancel(NOTIFICATION_ID_UPDATE_STATUS) + } + + + private fun sendDirectMessage(builder: NotificationCompat.Builder, + accountKey: UserKey, + recipientId: String, + text: String, + imageUri: String?): SingleResponse { + val credentials = ParcelableCredentialsUtils.getCredentials(this, + accountKey) ?: return SingleResponse.getInstance() + val twitter = MicroBlogAPIFactory.getInstance(this, credentials, true, true) + val twitterUpload = MicroBlogAPIFactory.getInstance(this, credentials, + true, true, TwitterUpload::class.java) + if (twitter == null || twitterUpload == null) return SingleResponse.getInstance() + try { + val directMessage: ParcelableDirectMessage + when (ParcelableAccountUtils.getAccountType(credentials)) { + ParcelableAccount.Type.FANFOU -> { + if (imageUri != null) { + throw MicroBlogException("Can't send image DM on Fanfou") + } + val dm = twitter.sendFanfouDirectMessage(recipientId, text) + directMessage = ParcelableDirectMessageUtils.fromDirectMessage(dm, accountKey, true) + } + else -> { + if (imageUri != null) { + val mediaUri = Uri.parse(imageUri) + var body: FileBody? = null + try { + body = UpdateStatusTask.getBodyFromMedia(contentResolver, + mediaUri, null, MessageMediaUploadListener(this, + notificationManager, builder, text)) + val uploadResp = uploadMedia(twitterUpload, body) + val response = twitter.sendDirectMessage(recipientId, + text, uploadResp.id) + directMessage = ParcelableDirectMessageUtils.fromDirectMessage(response, + accountKey, true) + } finally { + Utils.closeSilently(body) + } + val path = Utils.getImagePathFromUri(this, mediaUri) + if (path != null) { + val file = File(path) + if (!file.delete()) { + Log.d(LOGTAG, String.format("unable to delete %s", path)) + } + } + } else { + val response = twitter.sendDirectMessage(recipientId, text) + directMessage = ParcelableDirectMessageUtils.fromDirectMessage(response, + accountKey, true) + } + } + } + Utils.setLastSeen(this, UserKey(recipientId, accountKey.host), + System.currentTimeMillis()) + + return SingleResponse.getInstance(directMessage) + } catch (e: IOException) { + return SingleResponse.getInstance(e) + } catch (e: MicroBlogException) { + return SingleResponse.getInstance(e) + } + + } + + + @Throws(IOException::class, MicroBlogException::class) + private fun uploadMedia(upload: TwitterUpload, body: Body): MediaUploadResponse { + val mediaType = body.contentType().contentType + val length = body.length() + val stream = body.stream() + var response = upload.initUploadMedia(mediaType, length, null) + val segments = if (length == 0L) 0 else (length / BULK_SIZE + 1).toInt() + for (segmentIndex in 0..segments - 1) { + val currentBulkSize = Math.min(BULK_SIZE, length - segmentIndex * BULK_SIZE).toInt() + val bulk = SimpleBody(ContentType.OCTET_STREAM, null, currentBulkSize.toLong(), + stream) + upload.appendUploadMedia(response.id, segmentIndex, bulk) + } + response = upload.finalizeUploadMedia(response.id) + run { + var info: ProcessingInfo? = response.processingInfo + while (info != null && shouldWaitForProcess(info)) { + val checkAfterSecs = info.checkAfterSecs + if (checkAfterSecs <= 0) { + break + } + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(checkAfterSecs)) + } catch (e: InterruptedException) { + break + } + + response = upload.getUploadMediaStatus(response.id) + info = response.processingInfo + } + } + val info = response.processingInfo + if (info != null && ProcessingInfo.State.FAILED == info.state) { + val exception = MicroBlogException() + val errorInfo = info.error + if (errorInfo != null) { + exception.errors = arrayOf(errorInfo) + } + throw exception + } + return response + } + + private fun shouldWaitForProcess(info: ProcessingInfo): Boolean { + when (info.state) { + ProcessingInfo.State.PENDING, ProcessingInfo.State.IN_PROGRESS -> return true + else -> return false + } + } + + internal class MessageMediaUploadListener(private val context: Context, private val manager: NotificationManagerWrapper, + builder: NotificationCompat.Builder, private val message: String) : ReadListener { + + var percent: Int = 0 + + private val builder: Builder + + init { + this.builder = builder + } + + override fun onRead(length: Long, position: Long) { + val percent = if (length > 0) (position * 100 / length).toInt() else 0 + if (this.percent != percent) { + manager.notify(NOTIFICATION_ID_SEND_DIRECT_MESSAGE, + updateSendDirectMessageNotification(context, builder, percent, message)) + } + this.percent = percent + } + } + + companion object { + private val BULK_SIZE = (128 * 1024).toLong() // 128KiB + + private fun updateSendDirectMessageNotification(context: Context, + builder: NotificationCompat.Builder, + progress: Int, message: String?): Notification { + 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 fun updateUpdateStatusNotification(context: Context, + builder: NotificationCompat.Builder, + progress: Int, + status: ParcelableStatusUpdate?): Notification { + 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() + } + + fun updateStatusesAsync(context: Context, @Draft.Action action: String, + vararg statuses: ParcelableStatusUpdate) { + val intent = Intent(context, BackgroundOperationService::class.java) + intent.action = INTENT_ACTION_UPDATE_STATUS + intent.putExtra(EXTRA_STATUSES, statuses) + intent.putExtra(EXTRA_ACTION, action) + context.startService(intent) + } + } + +}