From 6463922d0e19578b7cd25edafd2ab315543719cc Mon Sep 17 00:00:00 2001 From: Mariotaku Lee Date: Thu, 14 Jul 2016 20:00:27 +0800 Subject: [PATCH] updated rest library --- twidere.component.common/build.gradle | 4 +- twidere/build.gradle | 4 +- .../library/twitter/UserStreamCallback.java | 2 +- .../activity/BrowserSignInActivity.java | 4 +- .../service/BackgroundOperationService.java | 19 +- .../task/twitter/UpdateStatusTask.java | 749 ------------------ .../twidere/task/twitter/UpdateStatusTask.kt | 672 ++++++++++++++++ .../twidere/activity/SignInActivity.kt | 2 +- .../fragment/BaseSupportWebViewFragment.kt | 6 +- .../fragment/ExternalBrowserPageFragment.kt | 2 +- .../twidere/fragment/UserFragment.kt | 15 +- 11 files changed, 699 insertions(+), 780 deletions(-) delete mode 100644 twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.java create mode 100644 twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt diff --git a/twidere.component.common/build.gradle b/twidere.component.common/build.gradle index a8654e9ad..5bb65a897 100644 --- a/twidere.component.common/build.gradle +++ b/twidere.component.common/build.gradle @@ -41,8 +41,8 @@ dependencies { apt 'com.github.mariotaku.ObjectCursor:processor:0.9.9' compile 'com.android.support:support-annotations:24.0.0' compile 'com.bluelinelabs:logansquare:1.3.7' - compile 'com.github.mariotaku.RestFu:library:0.9.30' - compile 'com.github.mariotaku.RestFu:oauth:0.9.30' + compile 'com.github.mariotaku.RestFu:library:0.9.31' + compile 'com.github.mariotaku.RestFu:oauth:0.9.31' compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.2' compile 'com.github.mariotaku.ObjectCursor:core:0.9.9' compile 'com.github.mariotaku.CommonsLibrary:objectcursor:0.9.8' diff --git a/twidere/build.gradle b/twidere/build.gradle index 22e659b5a..cef44df43 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -151,8 +151,8 @@ dependencies { compile 'com.soundcloud.android:android-crop:1.0.1@aar' compile 'com.hannesdorfmann.parcelableplease:annotation:1.0.2' compile 'com.github.mariotaku:PickNCrop:0.9.4' - compile 'com.github.mariotaku.RestFu:library:0.9.29' - compile 'com.github.mariotaku.RestFu:okhttp3:0.9.29' + compile 'com.github.mariotaku.RestFu:library:0.9.31' + compile 'com.github.mariotaku.RestFu:okhttp3:0.9.31' compile 'com.squareup.okhttp3:okhttp:3.2.0' compile 'com.lnikkila:extendedtouchview:0.1.0' compile 'com.google.dagger:dagger:2.1' diff --git a/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java b/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java index 9f5d1256b..4ec078e14 100644 --- a/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java +++ b/twidere/src/main/java/org/mariotaku/microblog/library/twitter/UserStreamCallback.java @@ -157,7 +157,7 @@ public abstract class UserStreamCallback implements RawCallback { } @Override - public final void error(final Throwable cause) { + public final void error(final Exception cause) { onException(cause); } diff --git a/twidere/src/main/java/org/mariotaku/twidere/activity/BrowserSignInActivity.java b/twidere/src/main/java/org/mariotaku/twidere/activity/BrowserSignInActivity.java index a04293a4a..45ff955a6 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/activity/BrowserSignInActivity.java +++ b/twidere/src/main/java/org/mariotaku/twidere/activity/BrowserSignInActivity.java @@ -133,8 +133,8 @@ public class BrowserSignInActivity extends BaseActivity { private String readOAuthPin(final String html) { try { OAuthPasswordAuthenticator.OAuthPinData data = new OAuthPasswordAuthenticator.OAuthPinData(); - OAuthPasswordAuthenticator.readOAuthPINFromHtml(new StringReader(html), data); - return data.oauthPin; + OAuthPasswordAuthenticator.Companion.readOAuthPINFromHtml(new StringReader(html), data); + return data.getOauthPin(); } catch (final AttoParseException | IOException e) { Log.w(LOGTAG, e); } diff --git a/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java b/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java index d98d4cdc1..c42491d98 100644 --- a/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java +++ b/twidere/src/main/java/org/mariotaku/twidere/service/BackgroundOperationService.java @@ -363,10 +363,13 @@ public class BackgroundOperationService extends IntentService implements Constan @Override public void afterExecute(Context handler, UpdateStatusTask.UpdateStatusResult result) { boolean failed = false; - if (result.exception != null) { - Toast.makeText(context, result.exception.getMessage(), Toast.LENGTH_SHORT).show(); + 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; - } else for (MicroBlogException e : result.exceptions) { + Log.w(LOGTAG, exception); + } else for (MicroBlogException e : exceptions) { if (e != null) { // Show error String errorMessage = Utils.getErrorMessage(context, e); @@ -407,9 +410,11 @@ public class BackgroundOperationService extends IntentService implements Constan } }); - if (result.exception != null) { - Log.w(LOGTAG, result.exception); - } else for (ParcelableStatus status : result.statuses) { + 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); @@ -457,7 +462,7 @@ public class BackgroundOperationService extends IntentService implements Constan final Uri mediaUri = Uri.parse(imageUri); FileBody body = null; try { - body = UpdateStatusTask.getBodyFromMedia(getContentResolver(), mediaUri, + body = UpdateStatusTask.Companion.getBodyFromMedia(getContentResolver(), mediaUri, new MessageMediaUploadListener(this, mNotificationManager, builder, text)); final MediaUploadResponse uploadResp = uploadMedia(twitterUpload, body); diff --git a/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.java b/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.java deleted file mode 100644 index 0ece12372..000000000 --- a/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.java +++ /dev/null @@ -1,749 +0,0 @@ -package org.mariotaku.twidere.task.twitter; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.UiThread; -import android.support.annotation.WorkerThread; -import android.text.TextUtils; -import android.util.Pair; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.mariotaku.abstask.library.AbstractTask; -import org.mariotaku.microblog.library.MicroBlog; -import org.mariotaku.microblog.library.MicroBlogException; -import org.mariotaku.microblog.library.fanfou.model.PhotoStatusUpdate; -import org.mariotaku.microblog.library.twitter.TwitterUpload; -import org.mariotaku.microblog.library.twitter.model.ErrorInfo; -import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse; -import org.mariotaku.microblog.library.twitter.model.Status; -import org.mariotaku.microblog.library.twitter.model.StatusUpdate; -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.app.TwidereApplication; -import org.mariotaku.twidere.model.Draft; -import org.mariotaku.twidere.model.DraftValuesCreator; -import org.mariotaku.twidere.model.MediaUploadResult; -import org.mariotaku.twidere.model.ParcelableAccount; -import org.mariotaku.twidere.model.ParcelableMediaUpdate; -import org.mariotaku.twidere.model.ParcelableStatus; -import org.mariotaku.twidere.model.ParcelableStatusUpdate; -import org.mariotaku.twidere.model.StatusShortenResult; -import org.mariotaku.twidere.model.UploaderMediaItem; -import org.mariotaku.twidere.model.UserKey; -import org.mariotaku.twidere.model.draft.UpdateStatusActionExtra; -import org.mariotaku.twidere.model.util.ParcelableAccountUtils; -import org.mariotaku.twidere.model.util.ParcelableLocationUtils; -import org.mariotaku.twidere.model.util.ParcelableStatusUtils; -import org.mariotaku.twidere.preference.ServicePickerPreference; -import org.mariotaku.twidere.provider.TwidereDataStore.Drafts; -import org.mariotaku.twidere.util.AbsServiceInterface; -import org.mariotaku.twidere.util.AsyncTwitterWrapper; -import org.mariotaku.twidere.util.CollectionUtils; -import org.mariotaku.twidere.util.MediaUploaderInterface; -import org.mariotaku.twidere.util.MicroBlogAPIFactory; -import org.mariotaku.twidere.util.SharedPreferencesWrapper; -import org.mariotaku.twidere.util.StatusShortenerInterface; -import org.mariotaku.twidere.util.Utils; -import org.mariotaku.twidere.util.dagger.GeneralComponentHelper; -import org.mariotaku.twidere.util.io.ContentLengthInputStream; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; - -/** - * Created by mariotaku on 16/5/22. - */ -public class UpdateStatusTask extends AbstractTask, - UpdateStatusTask.UpdateStatusResult, Context> implements Constants { - - private static final int BULK_SIZE = 256 * 1024;// 128 Kib - - final Context context; - final StateCallback stateCallback; - - @Inject - @NonNull - AsyncTwitterWrapper twitterWrapper; - @Inject - @NonNull - SharedPreferencesWrapper preferences; - - public UpdateStatusTask(Context context, StateCallback stateCallback) { - GeneralComponentHelper.build(context).inject(this); - this.context = context; - this.stateCallback = stateCallback; - } - - @Override - protected UpdateStatusResult doLongOperation(Pair params) { - final long draftId = saveDraft(params.first, params.second); - twitterWrapper.addSendingDraftId(draftId); - try { - final UpdateStatusResult result = doUpdateStatus(params.second); - deleteOrUpdateDraft(params.second, result, draftId); - return result; - } catch (UpdateStatusException e) { - return new UpdateStatusResult(e); - } finally { - twitterWrapper.removeSendingDraftId(draftId); - } - } - - @Override - protected void beforeExecute() { - stateCallback.beforeExecute(); - } - - @Override - protected void afterExecute(Context handler, UpdateStatusResult result) { - stateCallback.afterExecute(handler, result); - } - - @NonNull - private UpdateStatusResult doUpdateStatus(ParcelableStatusUpdate update) throws UpdateStatusException { - final TwidereApplication app = TwidereApplication.Companion.getInstance(context); - final MediaUploaderInterface uploader = getMediaUploader(app); - final StatusShortenerInterface shortener = getStatusShortener(app); - - final PendingStatusUpdate pendingUpdate = PendingStatusUpdate.from(update); - - - uploadMedia(uploader, update, pendingUpdate); - shortenStatus(shortener, update, pendingUpdate); - - final UpdateStatusResult result; - try { - result = requestUpdateStatus(update, pendingUpdate); - } catch (IOException e) { - return new UpdateStatusResult(new UpdateStatusException(e)); - } - - mediaUploadCallback(uploader, pendingUpdate, result); - statusShortenCallback(shortener, pendingUpdate, result); - return result; - } - - private void deleteOrUpdateDraft(ParcelableStatusUpdate update, UpdateStatusResult result, long draftId) { - final String where = Expression.equalsArgs(Drafts._ID).getSQL(); - final String[] whereArgs = {String.valueOf(draftId)}; - boolean hasError = false; - List failedAccounts = new ArrayList<>(); - for (int i = 0; i < update.accounts.length; i++) { - Exception exception = result.exceptions[i]; - if (exception != null && !isDuplicate(exception)) { - hasError = true; - failedAccounts.add(update.accounts[i].account_key); - } - } - final ContentResolver cr = context.getContentResolver(); - if (hasError) { - ContentValues values = new ContentValues(); - values.put(Drafts.ACCOUNT_KEYS, CollectionUtils.toString(failedAccounts, ',', false)); - cr.update(Drafts.CONTENT_URI, values, where, whereArgs); - // TODO show error message - } else { - cr.delete(Drafts.CONTENT_URI, where, whereArgs); - } - } - - private void uploadMedia(@Nullable MediaUploaderInterface uploader, - @NonNull ParcelableStatusUpdate update, - @NonNull PendingStatusUpdate pendingUpdate) throws UploadException { - stateCallback.onStartUploadingMedia(); - if (uploader == null) { - uploadMediaWithDefaultProvider(update, pendingUpdate); - } else { - uploadMediaWithExtension(uploader, update, pendingUpdate); - } - } - - private void uploadMediaWithExtension(@NonNull MediaUploaderInterface uploader, - @NonNull ParcelableStatusUpdate update, - @NonNull PendingStatusUpdate pending) throws UploadException { - final UploaderMediaItem[] media; - try { - media = UploaderMediaItem.getFromStatusUpdate(context, update); - } catch (FileNotFoundException e) { - throw new UploadException(e); - } - Map sharedMedia = new HashMap<>(); - for (int i = 0; i < pending.length; i++) { - ParcelableAccount account = update.accounts[i]; - // Skip upload if shared media found - final UserKey accountKey = account.account_key; - MediaUploadResult uploadResult = sharedMedia.get(accountKey); - if (uploadResult == null) { - uploadResult = uploader.upload(update, accountKey, media); - if (uploadResult == null) { - // TODO error handling - continue; - } - pending.mediaUploadResults[i] = uploadResult; - if (uploadResult.shared_owners != null) { - for (UserKey sharedOwner : uploadResult.shared_owners) { - sharedMedia.put(sharedOwner, uploadResult); - } - } - } - // Override status text - pending.overrideTexts[i] = Utils.getMediaUploadStatus(context, - uploadResult.media_uris, pending.overrideTexts[i]); - } - } - - private void shortenStatus(@Nullable StatusShortenerInterface shortener, - ParcelableStatusUpdate update, - PendingStatusUpdate pending) { - if (shortener == null) return; - stateCallback.onShorteningStatus(); - Map sharedShortened = new HashMap<>(); - for (int i = 0; i < pending.length; i++) { - ParcelableAccount account = update.accounts[i]; - // Skip upload if this shared media found - final UserKey accountKey = account.account_key; - StatusShortenResult shortenResult = sharedShortened.get(accountKey); - if (shortenResult == null) { - shortenResult = shortener.shorten(update, accountKey, pending.overrideTexts[i]); - if (shortenResult == null) { - // TODO error handling - continue; - } - pending.statusShortenResults[i] = shortenResult; - if (shortenResult.shared_owners != null) { - for (UserKey sharedOwner : shortenResult.shared_owners) { - sharedShortened.put(sharedOwner, shortenResult); - } - } - } - // Override status text - pending.overrideTexts[i] = shortenResult.shortened; - } - } - - @NonNull - private UpdateStatusResult requestUpdateStatus(ParcelableStatusUpdate statusUpdate, PendingStatusUpdate pendingUpdate) throws IOException { - - stateCallback.onUpdatingStatus(); - - UpdateStatusResult result = new UpdateStatusResult(new ParcelableStatus[pendingUpdate.length], - new MicroBlogException[pendingUpdate.length]); - - for (int i = 0; i < pendingUpdate.length; i++) { - final ParcelableAccount account = statusUpdate.accounts[i]; - MicroBlog microBlog = MicroBlogAPIFactory.getInstance(context, account.account_key, true); - Body body = null; - try { - switch (ParcelableAccountUtils.getAccountType(account)) { - case ParcelableAccount.Type.FANFOU: { - // Call uploadPhoto if media present - if (!ArrayUtils.isEmpty(statusUpdate.media)) { - // Fanfou only allow one photo - if (statusUpdate.media.length > 1) { - result.exceptions[i] = new MicroBlogException( - context.getString(R.string.error_too_many_photos_fanfou)); - break; - } - body = getBodyFromMedia(context.getContentResolver(), - Uri.parse(statusUpdate.media[0].uri), new ContentLengthInputStream.ReadListener() { - @Override - public void onRead(long length, long position) { - stateCallback.onUploadingProgressChanged(-1, position, length); - } - }); - PhotoStatusUpdate photoUpdate = new PhotoStatusUpdate(body, - pendingUpdate.overrideTexts[i]); - final Status requestResult = microBlog.uploadPhoto(photoUpdate); - - result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult, - account.account_key, false); - } else { - final Status requestResult = twitterUpdateStatus(microBlog, - statusUpdate, pendingUpdate, pendingUpdate.overrideTexts[i], i); - - result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult, - account.account_key, false); - } - break; - } - default: { - final Status requestResult = twitterUpdateStatus(microBlog, statusUpdate, - pendingUpdate, pendingUpdate.overrideTexts[i], i); - - result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult, - account.account_key, false); - break; - } - } - } catch (MicroBlogException e) { - result.exceptions[i] = e; - } finally { - Utils.closeSilently(body); - } - } - return result; - } - - /** - * Calling Twitter's upload method. This method sets multiple owner for bandwidth saving - */ - private void uploadMediaWithDefaultProvider(ParcelableStatusUpdate update, PendingStatusUpdate pendingUpdate) - throws UploadException { - // Return empty array if no media attached - if (ArrayUtils.isEmpty(update.media)) return; - List ownersList = new ArrayList<>(); - List ownerIdsList = new ArrayList<>(); - for (ParcelableAccount item : update.accounts) { - if (ParcelableAccount.Type.TWITTER.equals(ParcelableAccountUtils.getAccountType(item))) { - // Add to owners list - ownersList.add(item.account_key); - ownerIdsList.add(item.account_key.getId()); - } - } - String[] ownerIds = ownerIdsList.toArray(new String[ownerIdsList.size()]); - for (int i = 0; i < pendingUpdate.length; i++) { - final ParcelableAccount account = update.accounts[i]; - String[] mediaIds; - switch (ParcelableAccountUtils.getAccountType(account)) { - case ParcelableAccount.Type.TWITTER: { - final TwitterUpload upload = MicroBlogAPIFactory.getInstance(context, - account.account_key, true, true, TwitterUpload.class); - if (pendingUpdate.sharedMediaIds != null) { - mediaIds = pendingUpdate.sharedMediaIds; - } else { - mediaIds = uploadAllMediaShared(upload, update, ownerIds, true); - pendingUpdate.sharedMediaIds = mediaIds; - } - break; - } - case ParcelableAccount.Type.FANFOU: { - // Nope, fanfou uses photo uploading API - mediaIds = null; - break; - } - case ParcelableAccount.Type.STATUSNET: { - // TODO use their native API - final TwitterUpload upload = MicroBlogAPIFactory.getInstance(context, - account.account_key, true, true, TwitterUpload.class); - mediaIds = uploadAllMediaShared(upload, update, ownerIds, false); - break; - } - default: { - mediaIds = null; - break; - } - } - pendingUpdate.mediaIds[i] = mediaIds; - } - pendingUpdate.sharedMediaOwners = ownersList.toArray(new UserKey[ownersList.size()]); - } - - private Status twitterUpdateStatus(MicroBlog microBlog, ParcelableStatusUpdate statusUpdate, - PendingStatusUpdate pendingUpdate, String overrideText, - int index) throws MicroBlogException { - final StatusUpdate status = new StatusUpdate(overrideText); - if (statusUpdate.in_reply_to_status != null) { - status.inReplyToStatusId(statusUpdate.in_reply_to_status.id); - } - if (statusUpdate.repost_status_id != null) { - status.setRepostStatusId(statusUpdate.repost_status_id); - } - if (statusUpdate.attachment_url != null) { - status.setAttachmentUrl(statusUpdate.attachment_url); - } - if (statusUpdate.location != null) { - status.location(ParcelableLocationUtils.toGeoLocation(statusUpdate.location)); - status.displayCoordinates(statusUpdate.display_coordinates); - } - final String[] mediaIds = pendingUpdate.mediaIds[index]; - if (mediaIds != null) { - status.mediaIds(mediaIds); - } - status.possiblySensitive(statusUpdate.is_possibly_sensitive); - return microBlog.updateStatus(status); - } - - private void statusShortenCallback(StatusShortenerInterface shortener, PendingStatusUpdate pendingUpdate, UpdateStatusResult updateResult) { - for (int i = 0; i < pendingUpdate.length; i++) { - final StatusShortenResult shortenResult = pendingUpdate.statusShortenResults[i]; - final ParcelableStatus status = updateResult.statuses[i]; - if (shortenResult == null || status == null) continue; - shortener.callback(shortenResult, status); - } - } - - private void mediaUploadCallback(MediaUploaderInterface uploader, PendingStatusUpdate pendingUpdate, UpdateStatusResult updateResult) { - for (int i = 0; i < pendingUpdate.length; i++) { - final MediaUploadResult uploadResult = pendingUpdate.mediaUploadResults[i]; - final ParcelableStatus status = updateResult.statuses[i]; - if (uploadResult == null || status == null) continue; - uploader.callback(uploadResult, status); - } - } - - @Nullable - private StatusShortenerInterface getStatusShortener(TwidereApplication app) throws UploaderNotFoundException, UploadException, ShortenerNotFoundException, ShortenException { - final String shortenerComponent = preferences.getString(KEY_STATUS_SHORTENER, null); - if (ServicePickerPreference.isNoneValue(shortenerComponent)) return null; - - final StatusShortenerInterface shortener = StatusShortenerInterface.getInstance(app, shortenerComponent); - if (shortener == null) throw new ShortenerNotFoundException(); - try { - shortener.checkService(new AbsServiceInterface.CheckServiceAction() { - @Override - public void check(@Nullable Bundle metaData) throws AbsServiceInterface.CheckServiceException { - if (metaData == null) throw new ExtensionVersionMismatchException(); - final String extensionVersion = metaData.getString(METADATA_KEY_EXTENSION_VERSION_STATUS_SHORTENER); - if (!TextUtils.equals(extensionVersion, context.getString(R.string.status_shortener_service_interface_version))) { - throw new ExtensionVersionMismatchException(); - } - } - }); - } catch (AbsServiceInterface.CheckServiceException e) { - if (e instanceof ExtensionVersionMismatchException) { - throw new ShortenException(context.getString(R.string.shortener_version_incompatible)); - } - throw new ShortenException(e); - } - return shortener; - } - - @Nullable - private MediaUploaderInterface getMediaUploader(TwidereApplication app) throws UploaderNotFoundException, UploadException { - final String uploaderComponent = preferences.getString(KEY_MEDIA_UPLOADER, null); - if (ServicePickerPreference.isNoneValue(uploaderComponent)) return null; - final MediaUploaderInterface uploader = MediaUploaderInterface.getInstance(app, uploaderComponent); - if (uploader == null) { - throw new UploaderNotFoundException(context.getString(R.string.error_message_media_uploader_not_found)); - } - try { - uploader.checkService(new AbsServiceInterface.CheckServiceAction() { - @Override - public void check(@Nullable Bundle metaData) throws AbsServiceInterface.CheckServiceException { - if (metaData == null) throw new ExtensionVersionMismatchException(); - final String extensionVersion = metaData.getString(METADATA_KEY_EXTENSION_VERSION_MEDIA_UPLOADER); - if (!TextUtils.equals(extensionVersion, context.getString(R.string.media_uploader_service_interface_version))) { - throw new ExtensionVersionMismatchException(); - } - } - }); - } catch (AbsServiceInterface.CheckServiceException e) { - if (e instanceof ExtensionVersionMismatchException) { - throw new UploadException(context.getString(R.string.uploader_version_incompatible)); - } - throw new UploadException(e); - } - return uploader; - } - - @NonNull - private String[] uploadAllMediaShared(TwitterUpload upload, ParcelableStatusUpdate update, - String[] ownerIds, boolean chucked) throws UploadException { - String[] mediaIds = new String[update.media.length]; - for (int i = 0; i < update.media.length; i++) { - ParcelableMediaUpdate media = update.media[i]; - final MediaUploadResponse resp; - //noinspection TryWithIdenticalCatches - Body body = null; - try { - final int index = i; - body = getBodyFromMedia(context.getContentResolver(), Uri.parse(media.uri), new ContentLengthInputStream.ReadListener() { - @Override - public void onRead(long length, long position) { - stateCallback.onUploadingProgressChanged(index, position, length); - } - }); - if (chucked) { - resp = uploadMediaChucked(upload, body, ownerIds); - } else { - resp = upload.uploadMedia(body, ownerIds); - } - } catch (IOException e) { - throw new UploadException(e); - } catch (MicroBlogException e) { - throw new UploadException(e); - } finally { - Utils.closeSilently(body); - } - mediaIds[i] = resp.getId(); - } - return mediaIds; - } - - - private MediaUploadResponse uploadMediaChucked(final TwitterUpload upload, Body body, - String[] ownerIds) 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, ownerIds); - 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 (MediaUploadResponse.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()); - } - MediaUploadResponse.ProcessingInfo info = response.getProcessingInfo(); - if (info != null && MediaUploadResponse.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; - } - - - public static FileBody getBodyFromMedia(@NonNull final ContentResolver resolver, - @NonNull final Uri mediaUri, - @NonNull final ContentLengthInputStream.ReadListener - readListener) throws IOException { - final String mediaType = resolver.getType(mediaUri); - final InputStream is = resolver.openInputStream(mediaUri); - if (is == null) { - throw new FileNotFoundException(mediaUri.toString()); - } - final long length = is.available(); - final ContentLengthInputStream cis = new ContentLengthInputStream(is, length); - cis.setReadListener(readListener); - final ContentType contentType; - if (TextUtils.isEmpty(mediaType)) { - contentType = ContentType.parse("application/octet-stream"); - } else { - contentType = ContentType.parse(mediaType); - } - return new FileBody(cis, "attachment", length, contentType); - } - - private boolean isDuplicate(Exception exception) { - return exception instanceof MicroBlogException - && ((MicroBlogException) exception).getErrorCode() == ErrorInfo.STATUS_IS_DUPLICATE; - } - - private boolean shouldWaitForProcess(MediaUploadResponse.ProcessingInfo info) { - if (info == null) return false; - switch (info.getState()) { - case MediaUploadResponse.ProcessingInfo.State.PENDING: - case MediaUploadResponse.ProcessingInfo.State.IN_PROGRESS: - return true; - default: - return false; - } - } - - - private long saveDraft(String draftAction, ParcelableStatusUpdate statusUpdate) { - final Draft draft = new Draft(); - draft.account_keys = ParcelableAccountUtils.getAccountKeys(statusUpdate.accounts); - if (draftAction != null) { - draft.action_type = draftAction; - } else { - draft.action_type = Draft.Action.UPDATE_STATUS; - } - draft.text = statusUpdate.text; - draft.location = statusUpdate.location; - draft.media = statusUpdate.media; - final UpdateStatusActionExtra extra = new UpdateStatusActionExtra(); - extra.setInReplyToStatus(statusUpdate.in_reply_to_status); - extra.setIsPossiblySensitive(statusUpdate.is_possibly_sensitive); - extra.setRepostStatusId(statusUpdate.repost_status_id); - extra.setDisplayCoordinates(statusUpdate.display_coordinates); - draft.action_extras = extra; - final ContentResolver resolver = context.getContentResolver(); - final Uri draftUri = resolver.insert(Drafts.CONTENT_URI, DraftValuesCreator.create(draft)); - if (draftUri == null) return -1; - return NumberUtils.toLong(draftUri.getLastPathSegment(), -1); - } - - static class PendingStatusUpdate { - - @Nullable - String[] sharedMediaIds; - UserKey[] sharedMediaOwners; - - final int length; - - @NonNull - final String[] overrideTexts; - @NonNull - final String[][] mediaIds; - - @NonNull - final MediaUploadResult[] mediaUploadResults; - @NonNull - final StatusShortenResult[] statusShortenResults; - - PendingStatusUpdate(int length, String defaultText) { - this.length = length; - overrideTexts = new String[length]; - mediaUploadResults = new MediaUploadResult[length]; - statusShortenResults = new StatusShortenResult[length]; - mediaIds = new String[length][]; - Arrays.fill(overrideTexts, defaultText); - } - - static PendingStatusUpdate from(ParcelableStatusUpdate statusUpdate) { - return new PendingStatusUpdate(statusUpdate.accounts.length, - statusUpdate.text); - } - } - - public static class UpdateStatusResult { - @NonNull - public final ParcelableStatus[] statuses; - @NonNull - public final MicroBlogException[] exceptions; - - public final UpdateStatusException exception; - - public UpdateStatusResult(@NonNull ParcelableStatus[] statuses, @NonNull MicroBlogException[] exceptions) { - this.statuses = statuses; - this.exceptions = exceptions; - this.exception = null; - } - - public UpdateStatusResult(UpdateStatusException exception) { - this.exception = exception; - this.statuses = new ParcelableStatus[0]; - this.exceptions = new MicroBlogException[0]; - } - } - - - public static class UpdateStatusException extends Exception { - public UpdateStatusException() { - super(); - } - - public UpdateStatusException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public UpdateStatusException(Throwable throwable) { - super(throwable); - } - - public UpdateStatusException(final String message) { - super(message); - } - } - - public static class UploaderNotFoundException extends UpdateStatusException { - - public UploaderNotFoundException() { - super(); - } - - public UploaderNotFoundException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public UploaderNotFoundException(Throwable throwable) { - super(throwable); - } - - public UploaderNotFoundException(String message) { - super(message); - } - } - - public static class UploadException extends UpdateStatusException { - - public UploadException() { - super(); - } - - public UploadException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public UploadException(Throwable throwable) { - super(throwable); - } - - public UploadException(String message) { - super(message); - } - } - - public static class ExtensionVersionMismatchException extends AbsServiceInterface.CheckServiceException { - - } - - public static class ShortenerNotFoundException extends UpdateStatusException { - } - - public static class ShortenException extends UpdateStatusException { - - public ShortenException() { - super(); - } - - public ShortenException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public ShortenException(Throwable throwable) { - super(throwable); - } - - public ShortenException(final String message) { - super(message); - } - } - - public interface StateCallback { - @WorkerThread - void onStartUploadingMedia(); - - @WorkerThread - void onUploadingProgressChanged(int index, long current, long total); - - @WorkerThread - void onShorteningStatus(); - - @WorkerThread - void onUpdatingStatus(); - - @UiThread - void afterExecute(Context handler, UpdateStatusResult result); - - @UiThread - void beforeExecute(); - } -} 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 new file mode 100644 index 000000000..e5648b0cd --- /dev/null +++ b/twidere/src/main/java/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt @@ -0,0 +1,672 @@ +package org.mariotaku.twidere.task.twitter + +import android.content.ContentResolver +import android.content.ContentValues +import android.content.Context +import android.net.Uri +import android.support.annotation.UiThread +import android.support.annotation.WorkerThread +import android.text.TextUtils +import android.util.Pair +import org.apache.commons.lang3.ArrayUtils +import org.apache.commons.lang3.math.NumberUtils +import org.mariotaku.abstask.library.AbstractTask +import org.mariotaku.microblog.library.MicroBlog +import org.mariotaku.microblog.library.MicroBlogException +import org.mariotaku.microblog.library.fanfou.model.PhotoStatusUpdate +import org.mariotaku.microblog.library.twitter.TwitterUpload +import org.mariotaku.microblog.library.twitter.model.ErrorInfo +import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse +import org.mariotaku.microblog.library.twitter.model.Status +import org.mariotaku.microblog.library.twitter.model.StatusUpdate +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.TwidereConstants.* +import org.mariotaku.twidere.app.TwidereApplication +import org.mariotaku.twidere.constant.SharedPreferenceConstants +import org.mariotaku.twidere.constant.SharedPreferenceConstants.* +import org.mariotaku.twidere.model.* +import org.mariotaku.twidere.model.draft.UpdateStatusActionExtra +import org.mariotaku.twidere.model.util.ParcelableAccountUtils +import org.mariotaku.twidere.model.util.ParcelableLocationUtils +import org.mariotaku.twidere.model.util.ParcelableStatusUtils +import org.mariotaku.twidere.preference.ServicePickerPreference +import org.mariotaku.twidere.provider.TwidereDataStore.Drafts +import org.mariotaku.twidere.util.* +import org.mariotaku.twidere.util.dagger.GeneralComponentHelper +import org.mariotaku.twidere.util.io.ContentLengthInputStream +import java.io.FileNotFoundException +import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +/** + * Created by mariotaku on 16/5/22. + */ +class UpdateStatusTask(internal val context: Context, internal val stateCallback: UpdateStatusTask.StateCallback) : AbstractTask, UpdateStatusTask.UpdateStatusResult, Context>(), Constants { + + @Inject + lateinit var twitterWrapper: AsyncTwitterWrapper + @Inject + lateinit var preferences: SharedPreferencesWrapper + + init { + GeneralComponentHelper.build(context).inject(this) + } + + override fun doLongOperation(params: Pair): UpdateStatusResult { + val draftId = saveDraft(params.first, params.second) + twitterWrapper.addSendingDraftId(draftId) + try { + val result = doUpdateStatus(params.second) + deleteOrUpdateDraft(params.second, result, draftId) + return result + } catch (e: UpdateStatusException) { + return UpdateStatusResult(e) + } finally { + twitterWrapper.removeSendingDraftId(draftId) + } + } + + override fun beforeExecute() { + stateCallback.beforeExecute() + } + + override fun afterExecute(handler: Context?, result: UpdateStatusResult?) { + stateCallback.afterExecute(handler, result) + } + + @Throws(UpdateStatusException::class) + private fun doUpdateStatus(update: ParcelableStatusUpdate): UpdateStatusResult { + val app = TwidereApplication.getInstance(context) + val uploader = getMediaUploader(app) + val shortener = getStatusShortener(app) + + val pendingUpdate = PendingStatusUpdate.from(update) + + + uploadMedia(uploader, update, pendingUpdate) + shortenStatus(shortener, update, pendingUpdate) + + val result: UpdateStatusResult + try { + result = requestUpdateStatus(update, pendingUpdate) + } catch (e: IOException) { + return UpdateStatusResult(UpdateStatusException(e)) + } + + mediaUploadCallback(uploader, pendingUpdate, result) + statusShortenCallback(shortener, pendingUpdate, result) + return result + } + + private fun deleteOrUpdateDraft(update: ParcelableStatusUpdate, result: UpdateStatusResult, draftId: Long) { + val where = Expression.equalsArgs(Drafts._ID).sql + val whereArgs = arrayOf(draftId.toString()) + var hasError = false + val failedAccounts = ArrayList() + for (i in update.accounts.indices) { + val exception = result.exceptions[i] + if (exception != null && !isDuplicate(exception)) { + hasError = true + failedAccounts.add(update.accounts[i].account_key) + } + } + val cr = context.contentResolver + if (hasError) { + val values = ContentValues() + values.put(Drafts.ACCOUNT_KEYS, CollectionUtils.toString(failedAccounts, ',', false)) + cr.update(Drafts.CONTENT_URI, values, where, whereArgs) + // TODO show error message + } else { + cr.delete(Drafts.CONTENT_URI, where, whereArgs) + } + } + + @Throws(UploadException::class) + private fun uploadMedia(uploader: MediaUploaderInterface?, + update: ParcelableStatusUpdate, + pendingUpdate: PendingStatusUpdate) { + stateCallback.onStartUploadingMedia() + if (uploader == null) { + uploadMediaWithDefaultProvider(update, pendingUpdate) + } else { + uploadMediaWithExtension(uploader, update, pendingUpdate) + } + } + + @Throws(UploadException::class) + private fun uploadMediaWithExtension(uploader: MediaUploaderInterface, + update: ParcelableStatusUpdate, + pending: PendingStatusUpdate) { + val media: Array + try { + media = UploaderMediaItem.getFromStatusUpdate(context, update) + } catch (e: FileNotFoundException) { + throw UploadException(e) + } + + val sharedMedia = HashMap() + for (i in 0..pending.length - 1) { + val account = update.accounts[i] + // Skip upload if shared media found + val accountKey = account.account_key + var uploadResult: MediaUploadResult? = sharedMedia[accountKey] + if (uploadResult == null) { + uploadResult = uploader.upload(update, accountKey, media) + if (uploadResult == null) { + // TODO error handling + continue + } + pending.mediaUploadResults[i] = uploadResult + if (uploadResult.shared_owners != null) { + for (sharedOwner in uploadResult.shared_owners) { + sharedMedia.put(sharedOwner, uploadResult) + } + } + } + // Override status text + pending.overrideTexts[i] = Utils.getMediaUploadStatus(context, + uploadResult.media_uris, pending.overrideTexts[i]) + } + } + + private fun shortenStatus(shortener: StatusShortenerInterface?, + update: ParcelableStatusUpdate, + pending: PendingStatusUpdate) { + if (shortener == null) return + stateCallback.onShorteningStatus() + val sharedShortened = HashMap() + for (i in 0..pending.length - 1) { + val account = update.accounts[i] + // Skip upload if this shared media found + val accountKey = account.account_key + var shortenResult: StatusShortenResult? = sharedShortened[accountKey] + if (shortenResult == null) { + shortenResult = shortener.shorten(update, accountKey, pending.overrideTexts[i]) + if (shortenResult == null) { + // TODO error handling + continue + } + pending.statusShortenResults[i] = shortenResult + if (shortenResult.shared_owners != null) { + for (sharedOwner in shortenResult.shared_owners) { + sharedShortened.put(sharedOwner, shortenResult) + } + } + } + // Override status text + pending.overrideTexts[i] = shortenResult.shortened + } + } + + @Throws(IOException::class) + private fun requestUpdateStatus(statusUpdate: ParcelableStatusUpdate, pendingUpdate: PendingStatusUpdate): UpdateStatusResult { + + stateCallback.onUpdatingStatus() + + val result = UpdateStatusResult(arrayOfNulls(pendingUpdate.length), + arrayOfNulls(pendingUpdate.length)) + + for (i in 0..pendingUpdate.length - 1) { + val account = statusUpdate.accounts[i] + val microBlog = MicroBlogAPIFactory.getInstance(context, account.account_key, true) + var body: Body? = null + try { + when (ParcelableAccountUtils.getAccountType(account)) { + ParcelableAccount.Type.FANFOU -> { + // Call uploadPhoto if media present + if (!ArrayUtils.isEmpty(statusUpdate.media)) { + // Fanfou only allow one photo + if (statusUpdate.media.size > 1) { + result.exceptions[i] = MicroBlogException( + context.getString(R.string.error_too_many_photos_fanfou)) + } else { + body = getBodyFromMedia(context.contentResolver, + Uri.parse(statusUpdate.media[0].uri), ContentLengthInputStream.ReadListener { length, position -> stateCallback.onUploadingProgressChanged(-1, position, length) }) + val photoUpdate = PhotoStatusUpdate(body, + pendingUpdate.overrideTexts[i]) + val requestResult = microBlog.uploadPhoto(photoUpdate) + + result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult, + account.account_key, false) + } + } else { + val requestResult = twitterUpdateStatus(microBlog, + statusUpdate, pendingUpdate, pendingUpdate.overrideTexts[i], i) + + result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult, + account.account_key, false) + } + } + else -> { + val requestResult = twitterUpdateStatus(microBlog, statusUpdate, + pendingUpdate, pendingUpdate.overrideTexts[i], i) + + result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult, + account.account_key, false) + } + } + } catch (e: MicroBlogException) { + result.exceptions[i] = e + } finally { + Utils.closeSilently(body) + } + } + return result + } + + /** + * Calling Twitter's upload method. This method sets multiple owner for bandwidth saving + */ + @Throws(UploadException::class) + private fun uploadMediaWithDefaultProvider(update: ParcelableStatusUpdate, pendingUpdate: PendingStatusUpdate) { + // Return empty array if no media attached + if (ArrayUtils.isEmpty(update.media)) return + val ownersList = ArrayList() + val ownerIdsList = ArrayList() + for (item in update.accounts) { + if (ParcelableAccount.Type.TWITTER == ParcelableAccountUtils.getAccountType(item)) { + // Add to owners list + ownersList.add(item.account_key) + ownerIdsList.add(item.account_key.id) + } + } + val ownerIds = ownerIdsList.toTypedArray() + for (i in 0..pendingUpdate.length - 1) { + val account = update.accounts[i] + val mediaIds: Array? + when (ParcelableAccountUtils.getAccountType(account)) { + ParcelableAccount.Type.TWITTER -> { + val upload = MicroBlogAPIFactory.getInstance(context, + account.account_key, true, true, TwitterUpload::class.java)!! + if (pendingUpdate.sharedMediaIds != null) { + mediaIds = pendingUpdate.sharedMediaIds + } else { + mediaIds = uploadAllMediaShared(upload, update, ownerIds, true) + pendingUpdate.sharedMediaIds = mediaIds + } + } + ParcelableAccount.Type.FANFOU -> { + // Nope, fanfou uses photo uploading API + mediaIds = null + } + ParcelableAccount.Type.STATUSNET -> { + // TODO use their native API + val upload = MicroBlogAPIFactory.getInstance(context, + account.account_key, true, true, TwitterUpload::class.java)!! + mediaIds = uploadAllMediaShared(upload, update, ownerIds, false) + } + else -> { + mediaIds = null + } + } + pendingUpdate.mediaIds[i] = mediaIds + } + pendingUpdate.sharedMediaOwners = ownersList.toTypedArray() + } + + @Throws(MicroBlogException::class) + private fun twitterUpdateStatus(microBlog: MicroBlog, statusUpdate: ParcelableStatusUpdate, + pendingUpdate: PendingStatusUpdate, overrideText: String, + index: Int): Status { + val status = StatusUpdate(overrideText) + if (statusUpdate.in_reply_to_status != null) { + status.inReplyToStatusId(statusUpdate.in_reply_to_status.id) + } + if (statusUpdate.repost_status_id != null) { + status.setRepostStatusId(statusUpdate.repost_status_id) + } + if (statusUpdate.attachment_url != null) { + status.setAttachmentUrl(statusUpdate.attachment_url) + } + if (statusUpdate.location != null) { + status.location(ParcelableLocationUtils.toGeoLocation(statusUpdate.location)) + status.displayCoordinates(statusUpdate.display_coordinates) + } + val mediaIds = pendingUpdate.mediaIds[index] + if (mediaIds != null) { + status.mediaIds(*mediaIds) + } + status.possiblySensitive(statusUpdate.is_possibly_sensitive) + return microBlog.updateStatus(status) + } + + private fun statusShortenCallback(shortener: StatusShortenerInterface?, pendingUpdate: PendingStatusUpdate, updateResult: UpdateStatusResult) { + if (shortener == null) return + for (i in 0..pendingUpdate.length - 1) { + val shortenResult = pendingUpdate.statusShortenResults[i] + val status = updateResult.statuses[i] + if (shortenResult == null || status == null) continue + shortener.callback(shortenResult, status) + } + } + + private fun mediaUploadCallback(uploader: MediaUploaderInterface?, pendingUpdate: PendingStatusUpdate, updateResult: UpdateStatusResult) { + if (uploader == null) return + for (i in 0..pendingUpdate.length - 1) { + val uploadResult = pendingUpdate.mediaUploadResults[i] + val status = updateResult.statuses[i] + if (uploadResult == null || status == null) continue + uploader.callback(uploadResult, status) + } + } + + @Throws(UploaderNotFoundException::class, UploadException::class, ShortenerNotFoundException::class, ShortenException::class) + private fun getStatusShortener(app: TwidereApplication): StatusShortenerInterface? { + val shortenerComponent = preferences.getString(KEY_STATUS_SHORTENER, null) + if (ServicePickerPreference.isNoneValue(shortenerComponent)) return null + + val shortener = StatusShortenerInterface.getInstance(app, shortenerComponent) ?: throw ShortenerNotFoundException() + try { + shortener.checkService { metaData -> + if (metaData == null) throw ExtensionVersionMismatchException() + val extensionVersion = metaData.getString(METADATA_KEY_EXTENSION_VERSION_STATUS_SHORTENER) + if (!TextUtils.equals(extensionVersion, context.getString(R.string.status_shortener_service_interface_version))) { + throw ExtensionVersionMismatchException() + } + } + } catch (e: AbsServiceInterface.CheckServiceException) { + if (e is ExtensionVersionMismatchException) { + throw ShortenException(context.getString(R.string.shortener_version_incompatible)) + } + throw ShortenException(e) + } + + return shortener + } + + @Throws(UploaderNotFoundException::class, UploadException::class) + private fun getMediaUploader(app: TwidereApplication): MediaUploaderInterface? { + val uploaderComponent = preferences.getString(KEY_MEDIA_UPLOADER, null) + if (ServicePickerPreference.isNoneValue(uploaderComponent)) return null + val uploader = MediaUploaderInterface.getInstance(app, uploaderComponent) ?: throw UploaderNotFoundException(context.getString(R.string.error_message_media_uploader_not_found)) + try { + uploader.checkService { metaData -> + if (metaData == null) throw ExtensionVersionMismatchException() + val extensionVersion = metaData.getString(METADATA_KEY_EXTENSION_VERSION_MEDIA_UPLOADER) + if (!TextUtils.equals(extensionVersion, context.getString(R.string.media_uploader_service_interface_version))) { + throw ExtensionVersionMismatchException() + } + } + } catch (e: AbsServiceInterface.CheckServiceException) { + if (e is ExtensionVersionMismatchException) { + throw UploadException(context.getString(R.string.uploader_version_incompatible)) + } + throw UploadException(e) + } + + return uploader + } + + @Throws(UploadException::class) + private fun uploadAllMediaShared(upload: TwitterUpload, update: ParcelableStatusUpdate, + ownerIds: Array, chucked: Boolean): Array { + val mediaIds = update.media.mapIndexed { index, media -> + val resp: MediaUploadResponse + //noinspection TryWithIdenticalCatches + var body: Body? = null + try { + body = getBodyFromMedia(context.contentResolver, Uri.parse(media.uri), ContentLengthInputStream.ReadListener { length, position -> + stateCallback.onUploadingProgressChanged(index, position, length) + }) + if (chucked) { + resp = uploadMediaChucked(upload, body, ownerIds) + } else { + resp = upload.uploadMedia(body, ownerIds) + } + } catch (e: IOException) { + throw UploadException(e) + } catch (e: MicroBlogException) { + throw UploadException(e) + } finally { + Utils.closeSilently(body) + } + resp.id + } + return mediaIds.toTypedArray() + } + + + @Throws(IOException::class, MicroBlogException::class) + private fun uploadMediaChucked(upload: TwitterUpload, body: Body, + ownerIds: Array): MediaUploadResponse { + val mediaType = body.contentType().contentType + val length = body.length() + val stream = body.stream() + var response = upload.initUploadMedia(mediaType, length, ownerIds) + val segments = if (length == 0L) 0 else (length / BULK_SIZE + 1).toInt() + for (segmentIndex in 0..segments - 1) { + val currentBulkSize = Math.min(BULK_SIZE.toLong(), 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: MediaUploadResponse.ProcessingInfo = response.processingInfo + while (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 && MediaUploadResponse.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 isDuplicate(exception: Exception): Boolean { + return exception is MicroBlogException && exception.errorCode == ErrorInfo.STATUS_IS_DUPLICATE + } + + private fun shouldWaitForProcess(info: MediaUploadResponse.ProcessingInfo?): Boolean { + if (info == null) return false + when (info.state) { + MediaUploadResponse.ProcessingInfo.State.PENDING, MediaUploadResponse.ProcessingInfo.State.IN_PROGRESS -> return true + else -> return false + } + } + + + private fun saveDraft(draftAction: String?, statusUpdate: ParcelableStatusUpdate): Long { + val draft = Draft() + draft.account_keys = ParcelableAccountUtils.getAccountKeys(statusUpdate.accounts) + if (draftAction != null) { + draft.action_type = draftAction + } else { + draft.action_type = Draft.Action.UPDATE_STATUS + } + draft.text = statusUpdate.text + draft.location = statusUpdate.location + draft.media = statusUpdate.media + val extra = UpdateStatusActionExtra() + extra.inReplyToStatus = statusUpdate.in_reply_to_status + extra.setIsPossiblySensitive(statusUpdate.is_possibly_sensitive) + extra.isRepostStatusId = statusUpdate.repost_status_id + extra.displayCoordinates = statusUpdate.display_coordinates + draft.action_extras = extra + val resolver = context.contentResolver + val draftUri = resolver.insert(Drafts.CONTENT_URI, DraftValuesCreator.create(draft)) ?: return -1 + return NumberUtils.toLong(draftUri.lastPathSegment, -1) + } + + internal class PendingStatusUpdate(val length: Int, defaultText: String) { + + var sharedMediaIds: Array? = null + var sharedMediaOwners: Array? = null + + val overrideTexts: Array + val mediaIds: Array?> + + val mediaUploadResults: Array + val statusShortenResults: Array + + init { + overrideTexts = Array(length) { idx -> + defaultText + } + mediaUploadResults = arrayOfNulls(length) + statusShortenResults = arrayOfNulls(length) + mediaIds = arrayOfNulls>(length) + } + + companion object { + + fun from(statusUpdate: ParcelableStatusUpdate): PendingStatusUpdate { + return PendingStatusUpdate(statusUpdate.accounts.size, + statusUpdate.text) + } + } + } + + class UpdateStatusResult { + val statuses: Array + val exceptions: Array + + val exception: UpdateStatusException? + + constructor(statuses: Array, exceptions: Array) { + this.statuses = statuses + this.exceptions = exceptions + this.exception = null + } + + constructor(exception: UpdateStatusException) { + this.exception = exception + this.statuses = arrayOfNulls(0) + this.exceptions = arrayOfNulls(0) + } + } + + + open class UpdateStatusException : Exception { + constructor() : super() { + } + + constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) { + } + + constructor(throwable: Throwable) : super(throwable) { + } + + constructor(message: String) : super(message) { + } + } + + class UploaderNotFoundException : UpdateStatusException { + + constructor() : super() { + } + + constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) { + } + + constructor(throwable: Throwable) : super(throwable) { + } + + constructor(message: String) : super(message) { + } + } + + class UploadException : UpdateStatusException { + + constructor() : super() { + } + + constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) { + } + + constructor(throwable: Throwable) : super(throwable) { + } + + constructor(message: String) : super(message) { + } + } + + class ExtensionVersionMismatchException : AbsServiceInterface.CheckServiceException() + + class ShortenerNotFoundException : UpdateStatusException() + + class ShortenException : UpdateStatusException { + + constructor() : super() { + } + + constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable) { + } + + constructor(throwable: Throwable) : super(throwable) { + } + + constructor(message: String) : super(message) { + } + } + + interface StateCallback { + @WorkerThread + fun onStartUploadingMedia() + + @WorkerThread + fun onUploadingProgressChanged(index: Int, current: Long, total: Long) + + @WorkerThread + fun onShorteningStatus() + + @WorkerThread + fun onUpdatingStatus() + + @UiThread + fun afterExecute(handler: Context?, result: UpdateStatusResult?) + + @UiThread + fun beforeExecute() + } + + companion object { + + private val BULK_SIZE = 256 * 1024// 128 Kib + + + @Throws(IOException::class) + fun getBodyFromMedia(resolver: ContentResolver, + mediaUri: Uri, + readListener: ContentLengthInputStream.ReadListener): FileBody { + val mediaType = resolver.getType(mediaUri) + val `is` = resolver.openInputStream(mediaUri) ?: throw FileNotFoundException(mediaUri.toString()) + val length = `is`.available().toLong() + val cis = ContentLengthInputStream(`is`, length) + cis.setReadListener(readListener) + val contentType: ContentType + if (TextUtils.isEmpty(mediaType)) { + contentType = ContentType.parse("application/octet-stream") + } else { + contentType = ContentType.parse(mediaType!!) + } + return FileBody(cis, "attachment", length, contentType) + } + } +} diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt index c63cf67dd..251020220 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/activity/SignInActivity.kt @@ -935,7 +935,7 @@ class SignInActivity : BaseActivity(), OnClickListener, TextWatcher { field = value } - override fun getLoginVerification(challengeType: String?): String? { + override fun getLoginVerification(challengeType: String): String? { // Dismiss current progress dialog publishProgress(Runnable { val activity = activityRef.get() ?: return@Runnable diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseSupportWebViewFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseSupportWebViewFragment.kt index a4c38012e..8012620e8 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseSupportWebViewFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/BaseSupportWebViewFragment.kt @@ -90,10 +90,8 @@ open class BaseSupportWebViewFragment : BaseSupportFragment() { * Called when the fragment is no longer in use. Destroys the internal state of the WebView. */ override fun onDestroy() { - if (internalWebView != null) { - internalWebView!!.destroy() - internalWebView = null - } + internalWebView?.destroy() + internalWebView = null super.onDestroy() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ExternalBrowserPageFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ExternalBrowserPageFragment.kt index 2b9034553..de47d5b8f 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ExternalBrowserPageFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/ExternalBrowserPageFragment.kt @@ -51,7 +51,7 @@ class ExternalBrowserPageFragment : MediaViewerFragment() { } override fun onDestroy() { - webView.destroy() + webView?.destroy() super.onDestroy() } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt index 582a2e020..c9072cc19 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/fragment/UserFragment.kt @@ -510,7 +510,7 @@ class UserFragment : BaseSupportFragment(), OnClickListener, OnLinkClickListener return false } - fun getUserInfo(accountKey: UserKey?, userKey: UserKey?, screenName: String?, + fun getUserInfo(accountKey: UserKey, userKey: UserKey?, screenName: String?, omitIntentExtra: Boolean) { val lm = loaderManager lm.destroyLoader(LOADER_ID_USER) @@ -614,16 +614,9 @@ class UserFragment : BaseSupportFragment(), OnClickListener, OnLinkClickListener ThemeUtils.getUserThemeBackgroundAlpha(activity)) mActionBarShadowColor = 0xA0000000.toInt() val args = arguments - var accountId: UserKey? = null - var userId: UserKey? = null - var screenName: String? = null - if (savedInstanceState != null) { - args.putAll(savedInstanceState) - } else { - accountId = args.getParcelable(EXTRA_ACCOUNT_KEY) - userId = args.getParcelable(EXTRA_USER_KEY) - screenName = args.getString(EXTRA_SCREEN_NAME) - } + val accountId: UserKey = args.getParcelable(EXTRA_ACCOUNT_KEY) + val userId: UserKey? = args.getParcelable(EXTRA_USER_KEY) + val screenName: String? = args.getString(EXTRA_SCREEN_NAME) Utils.setNdefPushMessageCallback(activity, CreateNdefMessageCallback { val user = user ?: return@CreateNdefMessageCallback null