2016-07-14 14:00:27 +02:00
|
|
|
package org.mariotaku.twidere.task.twitter
|
|
|
|
|
2017-01-22 19:37:21 +01:00
|
|
|
import android.content.ContentResolver
|
2016-07-14 14:00:27 +02:00
|
|
|
import android.content.ContentValues
|
|
|
|
import android.content.Context
|
2016-07-16 09:27:46 +02:00
|
|
|
import android.graphics.Bitmap
|
|
|
|
import android.graphics.BitmapFactory
|
2016-07-30 02:08:57 +02:00
|
|
|
import android.graphics.Point
|
2017-01-23 14:31:14 +01:00
|
|
|
import android.media.MediaMetadataRetriever
|
2016-07-14 14:00:27 +02:00
|
|
|
import android.net.Uri
|
2017-01-23 06:27:29 +01:00
|
|
|
import android.os.Build
|
2016-07-14 14:00:27 +02:00
|
|
|
import android.support.annotation.UiThread
|
|
|
|
import android.support.annotation.WorkerThread
|
|
|
|
import android.text.TextUtils
|
2017-01-22 19:37:21 +01:00
|
|
|
import android.webkit.MimeTypeMap
|
2017-01-07 18:07:12 +01:00
|
|
|
import com.nostra13.universalimageloader.core.DisplayImageOptions
|
2016-09-01 09:04:54 +02:00
|
|
|
import com.nostra13.universalimageloader.core.assist.ImageSize
|
2016-10-05 15:12:33 +02:00
|
|
|
import edu.tsinghua.hotmobi.HotMobiLogger
|
|
|
|
import edu.tsinghua.hotmobi.model.MediaUploadEvent
|
2017-01-23 06:27:29 +01:00
|
|
|
import net.ypresto.androidtranscoder.MediaTranscoder
|
|
|
|
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets
|
2016-07-14 14:00:27 +02:00
|
|
|
import org.apache.commons.lang3.ArrayUtils
|
|
|
|
import org.apache.commons.lang3.math.NumberUtils
|
2017-01-24 12:10:24 +01:00
|
|
|
import org.mariotaku.ktextension.*
|
2016-07-14 14:00:27 +02:00
|
|
|
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
|
2016-09-17 15:44:53 +02:00
|
|
|
import org.mariotaku.microblog.library.twitter.model.*
|
2016-07-14 14:00:27 +02:00
|
|
|
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.R
|
|
|
|
import org.mariotaku.twidere.TwidereConstants.*
|
2016-12-03 06:48:40 +01:00
|
|
|
import org.mariotaku.twidere.annotation.AccountType
|
2016-07-14 14:00:27 +02:00
|
|
|
import org.mariotaku.twidere.app.TwidereApplication
|
2016-12-08 16:45:07 +01:00
|
|
|
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
|
2017-01-24 12:10:24 +01:00
|
|
|
import org.mariotaku.twidere.extension.model.size_limit
|
2016-07-14 14:00:27 +02:00
|
|
|
import org.mariotaku.twidere.model.*
|
2017-01-23 14:31:14 +01:00
|
|
|
import org.mariotaku.twidere.model.account.AccountExtras
|
2017-01-03 12:59:38 +01:00
|
|
|
import org.mariotaku.twidere.model.analyzer.UpdateStatus
|
2016-12-08 04:07:13 +01:00
|
|
|
import org.mariotaku.twidere.model.draft.UpdateStatusActionExtras
|
2016-07-14 14:00:27 +02:00
|
|
|
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
|
2017-02-07 15:55:36 +01:00
|
|
|
import org.mariotaku.twidere.task.BaseAbstractTask
|
2016-07-14 14:00:27 +02:00
|
|
|
import org.mariotaku.twidere.util.*
|
|
|
|
import org.mariotaku.twidere.util.io.ContentLengthInputStream
|
2017-01-24 12:10:24 +01:00
|
|
|
import java.io.Closeable
|
|
|
|
import java.io.File
|
|
|
|
import java.io.FileNotFoundException
|
|
|
|
import java.io.IOException
|
2016-07-14 14:00:27 +02:00
|
|
|
import java.util.*
|
|
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Created by mariotaku on 16/5/22.
|
|
|
|
*/
|
2016-08-17 05:40:15 +02:00
|
|
|
class UpdateStatusTask(
|
2017-02-07 15:55:36 +01:00
|
|
|
context: Context,
|
2016-08-17 05:40:15 +02:00
|
|
|
internal val stateCallback: UpdateStatusTask.StateCallback
|
2017-02-07 15:55:36 +01:00
|
|
|
) : BaseAbstractTask<Pair<String, ParcelableStatusUpdate>, UpdateStatusTask.UpdateStatusResult, Any?>(context) {
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
override fun doLongOperation(params: Pair<String, ParcelableStatusUpdate>): UpdateStatusResult {
|
|
|
|
val draftId = saveDraft(params.first, params.second)
|
2017-02-07 15:55:36 +01:00
|
|
|
microBlogWrapper.addSendingDraftId(draftId)
|
2016-07-14 14:00:27 +02:00
|
|
|
try {
|
2016-08-17 05:40:15 +02:00
|
|
|
val result = doUpdateStatus(params.second, draftId)
|
2016-07-14 14:00:27 +02:00
|
|
|
deleteOrUpdateDraft(params.second, result, draftId)
|
|
|
|
return result
|
|
|
|
} catch (e: UpdateStatusException) {
|
2016-08-17 05:40:15 +02:00
|
|
|
return UpdateStatusResult(e, draftId)
|
2016-07-14 14:00:27 +02:00
|
|
|
} finally {
|
2017-02-07 15:55:36 +01:00
|
|
|
microBlogWrapper.removeSendingDraftId(draftId)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun beforeExecute() {
|
|
|
|
stateCallback.beforeExecute()
|
|
|
|
}
|
|
|
|
|
2016-12-15 01:13:09 +01:00
|
|
|
override fun afterExecute(handler: Any?, result: UpdateStatusResult) {
|
|
|
|
stateCallback.afterExecute(result)
|
2017-01-03 12:59:38 +01:00
|
|
|
if (params != null) {
|
|
|
|
logUpdateStatus(params.first, params.second, result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun logUpdateStatus(actionType: String, statusUpdate: ParcelableStatusUpdate, result: UpdateStatusResult) {
|
|
|
|
val mediaType = statusUpdate.media?.firstOrNull()?.type ?: ParcelableMedia.Type.UNKNOWN
|
|
|
|
val hasLocation = statusUpdate.location != null
|
|
|
|
val preciseLocation = statusUpdate.display_coordinates
|
|
|
|
Analyzer.log(UpdateStatus(result.accountTypes.firstOrNull(), actionType, mediaType,
|
|
|
|
hasLocation, preciseLocation, result.succeed))
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(UpdateStatusException::class)
|
2016-08-17 05:40:15 +02:00
|
|
|
private fun doUpdateStatus(update: ParcelableStatusUpdate, draftId: Long): UpdateStatusResult {
|
2016-07-14 14:00:27 +02:00
|
|
|
val app = TwidereApplication.getInstance(context)
|
|
|
|
val uploader = getMediaUploader(app)
|
|
|
|
val shortener = getStatusShortener(app)
|
|
|
|
|
2017-01-23 07:12:29 +01:00
|
|
|
val pendingUpdate = PendingStatusUpdate(update)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
val result: UpdateStatusResult
|
|
|
|
try {
|
2017-01-23 07:41:36 +01:00
|
|
|
uploadMedia(uploader, update, pendingUpdate)
|
|
|
|
shortenStatus(shortener, update, pendingUpdate)
|
|
|
|
|
|
|
|
try {
|
|
|
|
result = requestUpdateStatus(update, pendingUpdate, draftId)
|
|
|
|
} catch (e: IOException) {
|
|
|
|
return UpdateStatusResult(UpdateStatusException(e), draftId)
|
|
|
|
}
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-01-23 07:41:36 +01:00
|
|
|
mediaUploadCallback(uploader, pendingUpdate, result)
|
|
|
|
statusShortenCallback(shortener, pendingUpdate, result)
|
2017-01-23 07:12:29 +01:00
|
|
|
|
2017-01-23 07:41:36 +01:00
|
|
|
// Cleanup
|
|
|
|
pendingUpdate.deleteOnSuccess.forEach { item -> item.delete(context) }
|
|
|
|
} finally {
|
|
|
|
// Cleanup
|
|
|
|
pendingUpdate.deleteAlways.forEach { item -> item.delete(context) }
|
2017-02-04 11:42:14 +01:00
|
|
|
uploader?.unbindService()
|
|
|
|
shortener?.unbindService()
|
2017-01-23 07:12:29 +01:00
|
|
|
}
|
2016-07-14 14:00:27 +02:00
|
|
|
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<UserKey>()
|
|
|
|
for (i in update.accounts.indices) {
|
|
|
|
val exception = result.exceptions[i]
|
|
|
|
if (exception != null && !isDuplicate(exception)) {
|
|
|
|
hasError = true
|
2016-12-04 04:58:03 +01:00
|
|
|
failedAccounts.add(update.accounts[i].key)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
val cr = context.contentResolver
|
|
|
|
if (hasError) {
|
|
|
|
val values = ContentValues()
|
2016-08-25 04:10:53 +02:00
|
|
|
values.put(Drafts.ACCOUNT_KEYS, failedAccounts.joinToString(","))
|
2016-07-14 14:00:27 +02:00
|
|
|
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) {
|
2016-12-07 14:20:25 +01:00
|
|
|
uploader.waitForService()
|
2016-07-14 14:00:27 +02:00
|
|
|
val media: Array<UploaderMediaItem>
|
|
|
|
try {
|
|
|
|
media = UploaderMediaItem.getFromStatusUpdate(context, update)
|
|
|
|
} catch (e: FileNotFoundException) {
|
|
|
|
throw UploadException(e)
|
|
|
|
}
|
|
|
|
|
|
|
|
val sharedMedia = HashMap<UserKey, MediaUploadResult>()
|
|
|
|
for (i in 0..pending.length - 1) {
|
|
|
|
val account = update.accounts[i]
|
|
|
|
// Skip upload if shared media found
|
2016-12-04 04:58:03 +01:00
|
|
|
val accountKey = account.key
|
2016-07-14 14:00:27 +02:00
|
|
|
var uploadResult: MediaUploadResult? = sharedMedia[accountKey]
|
|
|
|
if (uploadResult == null) {
|
2016-12-07 15:01:27 +01:00
|
|
|
uploadResult = uploader.upload(update, accountKey, media) ?: run {
|
|
|
|
throw UploadException()
|
|
|
|
}
|
|
|
|
if (uploadResult.media_uris == null) {
|
|
|
|
throw UploadException(uploadResult.error_message ?: "Unknown error")
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
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])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-07 15:01:27 +01:00
|
|
|
@Throws(UpdateStatusException::class)
|
2016-07-14 14:00:27 +02:00
|
|
|
private fun shortenStatus(shortener: StatusShortenerInterface?,
|
|
|
|
update: ParcelableStatusUpdate,
|
|
|
|
pending: PendingStatusUpdate) {
|
2016-12-08 08:47:17 +01:00
|
|
|
if (shortener == null) return
|
2016-07-14 14:00:27 +02:00
|
|
|
stateCallback.onShorteningStatus()
|
|
|
|
val sharedShortened = HashMap<UserKey, StatusShortenResult>()
|
2016-12-08 08:47:17 +01:00
|
|
|
for (i in 0 until pending.length) {
|
2016-07-14 14:00:27 +02:00
|
|
|
val account = update.accounts[i]
|
2016-12-08 08:47:17 +01:00
|
|
|
val text = pending.overrideTexts[i]
|
|
|
|
val textLimit = TwidereValidator.getTextLimit(account)
|
|
|
|
if (textLimit >= 0 && text.length <= textLimit) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
shortener.waitForService()
|
2016-07-14 14:00:27 +02:00
|
|
|
// Skip upload if this shared media found
|
2016-12-04 04:58:03 +01:00
|
|
|
val accountKey = account.key
|
2016-07-14 14:00:27 +02:00
|
|
|
var shortenResult: StatusShortenResult? = sharedShortened[accountKey]
|
|
|
|
if (shortenResult == null) {
|
2016-12-08 08:47:17 +01:00
|
|
|
shortenResult = shortener.shorten(update, accountKey, text) ?: run {
|
2016-12-07 15:01:27 +01:00
|
|
|
throw ShortenException()
|
|
|
|
}
|
|
|
|
if (shortenResult.shortened == null) {
|
|
|
|
throw ShortenException(shortenResult.error_message ?: "Unknown error")
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
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)
|
2016-08-17 05:40:15 +02:00
|
|
|
private fun requestUpdateStatus(statusUpdate: ParcelableStatusUpdate,
|
|
|
|
pendingUpdate: PendingStatusUpdate,
|
|
|
|
draftId: Long): UpdateStatusResult {
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
stateCallback.onUpdatingStatus()
|
|
|
|
|
2017-01-03 12:59:38 +01:00
|
|
|
val result = UpdateStatusResult(pendingUpdate.length, draftId)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
for (i in 0 until pendingUpdate.length) {
|
2016-07-14 14:00:27 +02:00
|
|
|
val account = statusUpdate.accounts[i]
|
2017-01-03 12:59:38 +01:00
|
|
|
result.accountTypes[i] = account.type
|
2016-12-06 06:15:22 +01:00
|
|
|
val microBlog = MicroBlogAPIFactory.getInstance(context, account.key)
|
2017-01-23 07:12:29 +01:00
|
|
|
var mediaBody: MediaStreamBody? = null
|
2016-07-14 14:00:27 +02:00
|
|
|
try {
|
2016-12-04 06:45:57 +01:00
|
|
|
when (account.type) {
|
2016-12-03 06:48:40 +01:00
|
|
|
AccountType.FANFOU -> {
|
2016-07-14 14:00:27 +02:00
|
|
|
// 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 {
|
2017-01-24 12:10:24 +01:00
|
|
|
val sizeLimit = account.size_limit
|
|
|
|
val firstMedia = statusUpdate.media.first()
|
|
|
|
mediaBody = getBodyFromMedia(context, mediaLoader, Uri.parse(firstMedia.uri),
|
|
|
|
sizeLimit, firstMedia.type, false, ContentLengthInputStream.ReadListener { length, position ->
|
|
|
|
stateCallback.onUploadingProgressChanged(-1, position, length)
|
|
|
|
})
|
2017-01-23 07:12:29 +01:00
|
|
|
val photoUpdate = PhotoStatusUpdate(mediaBody.body,
|
2016-07-14 14:00:27 +02:00
|
|
|
pendingUpdate.overrideTexts[i])
|
|
|
|
val requestResult = microBlog.uploadPhoto(photoUpdate)
|
|
|
|
|
|
|
|
result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult,
|
2016-12-04 04:58:03 +01:00
|
|
|
account.key, false)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
} else {
|
2017-01-23 07:12:29 +01:00
|
|
|
val requestResult = twitterUpdateStatus(microBlog, statusUpdate,
|
|
|
|
pendingUpdate, pendingUpdate.overrideTexts[i], i)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult,
|
2016-12-04 04:58:03 +01:00
|
|
|
account.key, false)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
val requestResult = twitterUpdateStatus(microBlog, statusUpdate,
|
|
|
|
pendingUpdate, pendingUpdate.overrideTexts[i], i)
|
|
|
|
|
|
|
|
result.statuses[i] = ParcelableStatusUtils.fromStatus(requestResult,
|
2016-12-04 04:58:03 +01:00
|
|
|
account.key, false)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e: MicroBlogException) {
|
|
|
|
result.exceptions[i] = e
|
|
|
|
} finally {
|
2017-01-23 07:12:29 +01:00
|
|
|
Utils.closeSilently(mediaBody)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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
|
2016-07-31 08:41:07 +02:00
|
|
|
val ownersList = update.accounts.filter {
|
2016-12-04 06:45:57 +01:00
|
|
|
AccountType.TWITTER == it.type
|
2016-12-04 04:58:03 +01:00
|
|
|
}.map(AccountDetails::key)
|
2016-07-31 08:41:07 +02:00
|
|
|
val ownerIds = ownersList.map {
|
|
|
|
it.id
|
|
|
|
}.toTypedArray()
|
2016-07-14 14:00:27 +02:00
|
|
|
for (i in 0..pendingUpdate.length - 1) {
|
|
|
|
val account = update.accounts[i]
|
|
|
|
val mediaIds: Array<String>?
|
2016-12-04 06:45:57 +01:00
|
|
|
when (account.type) {
|
2016-12-03 06:48:40 +01:00
|
|
|
AccountType.TWITTER -> {
|
2016-12-08 16:45:07 +01:00
|
|
|
val upload = account.newMicroBlogInstance(context, cls = TwitterUpload::class.java)
|
2016-07-14 14:00:27 +02:00
|
|
|
if (pendingUpdate.sharedMediaIds != null) {
|
|
|
|
mediaIds = pendingUpdate.sharedMediaIds
|
|
|
|
} else {
|
2017-01-23 14:31:14 +01:00
|
|
|
val (ids, deleteOnSuccess, deleteAlways) = uploadAllMediaShared(upload,
|
|
|
|
account, update, ownerIds, true)
|
2017-01-23 07:12:29 +01:00
|
|
|
mediaIds = ids
|
2017-01-23 07:41:36 +01:00
|
|
|
deleteOnSuccess.addAllTo(pendingUpdate.deleteOnSuccess)
|
|
|
|
deleteAlways.addAllTo(pendingUpdate.deleteAlways)
|
2016-07-14 14:00:27 +02:00
|
|
|
pendingUpdate.sharedMediaIds = mediaIds
|
|
|
|
}
|
|
|
|
}
|
2016-12-03 06:48:40 +01:00
|
|
|
AccountType.FANFOU -> {
|
2016-07-14 14:00:27 +02:00
|
|
|
// Nope, fanfou uses photo uploading API
|
|
|
|
mediaIds = null
|
|
|
|
}
|
2016-12-03 06:48:40 +01:00
|
|
|
AccountType.STATUSNET -> {
|
2016-07-14 14:00:27 +02:00
|
|
|
// TODO use their native API
|
2016-12-08 16:45:07 +01:00
|
|
|
val upload = account.newMicroBlogInstance(context, cls = TwitterUpload::class.java)
|
2017-01-23 14:31:14 +01:00
|
|
|
val (ids, deleteOnSuccess, deleteAlways) = uploadAllMediaShared(upload, account,
|
|
|
|
update, ownerIds, false)
|
2017-01-23 07:12:29 +01:00
|
|
|
mediaIds = ids
|
2017-01-23 07:41:36 +01:00
|
|
|
deleteOnSuccess.addAllTo(pendingUpdate.deleteOnSuccess)
|
|
|
|
deleteAlways.addAllTo(pendingUpdate.deleteAlways)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
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)
|
|
|
|
}
|
2017-01-22 14:31:25 +01:00
|
|
|
if (statusUpdate.is_possibly_sensitive) {
|
|
|
|
status.possiblySensitive(statusUpdate.is_possibly_sensitive)
|
|
|
|
}
|
2016-07-14 14:00:27 +02:00
|
|
|
return microBlog.updateStatus(status)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun statusShortenCallback(shortener: StatusShortenerInterface?, pendingUpdate: PendingStatusUpdate, updateResult: UpdateStatusResult) {
|
2016-12-07 15:01:27 +01:00
|
|
|
if (shortener == null || !shortener.waitForService()) return
|
2016-07-14 14:00:27 +02:00
|
|
|
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) {
|
2016-12-07 15:01:27 +01:00
|
|
|
if (uploader == null || !uploader.waitForService()) return
|
2016-07-14 14:00:27 +02:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
2016-12-15 13:37:55 +01:00
|
|
|
} catch (e: ExtensionVersionMismatchException) {
|
|
|
|
throw ShortenException(context.getString(R.string.shortener_version_incompatible))
|
2016-07-14 14:00:27 +02:00
|
|
|
} catch (e: AbsServiceInterface.CheckServiceException) {
|
|
|
|
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)
|
2017-01-23 07:12:29 +01:00
|
|
|
private fun uploadAllMediaShared(
|
|
|
|
upload: TwitterUpload,
|
2017-01-23 14:31:14 +01:00
|
|
|
account: AccountDetails,
|
2017-01-23 07:12:29 +01:00
|
|
|
update: ParcelableStatusUpdate,
|
2017-01-24 12:10:24 +01:00
|
|
|
ownerIds: Array<String>,
|
|
|
|
chucked: Boolean
|
2017-01-23 07:41:36 +01:00
|
|
|
): SharedMediaUploadResult {
|
2017-01-23 07:12:29 +01:00
|
|
|
val deleteOnSuccess = ArrayList<MediaDeletionItem>()
|
2017-01-23 07:41:36 +01:00
|
|
|
val deleteAlways = ArrayList<MediaDeletionItem>()
|
2016-07-14 14:00:27 +02:00
|
|
|
val mediaIds = update.media.mapIndexed { index, media ->
|
|
|
|
val resp: MediaUploadResponse
|
|
|
|
//noinspection TryWithIdenticalCatches
|
2017-01-23 07:12:29 +01:00
|
|
|
var body: MediaStreamBody? = null
|
2016-07-14 14:00:27 +02:00
|
|
|
try {
|
2017-01-24 12:10:24 +01:00
|
|
|
val sizeLimit = account.size_limit
|
2017-01-23 07:12:29 +01:00
|
|
|
body = getBodyFromMedia(context, mediaLoader, Uri.parse(media.uri), sizeLimit,
|
2017-01-24 12:10:24 +01:00
|
|
|
media.type, chucked, ContentLengthInputStream.ReadListener { length, position ->
|
2016-07-31 08:41:07 +02:00
|
|
|
stateCallback.onUploadingProgressChanged(index, position, length)
|
|
|
|
})
|
2016-10-05 15:12:33 +02:00
|
|
|
val mediaUploadEvent = MediaUploadEvent.create(context, media)
|
2017-01-23 07:12:29 +01:00
|
|
|
mediaUploadEvent.setFileSize(body.body.length())
|
|
|
|
body.geometry?.let { geometry ->
|
2017-01-23 06:27:29 +01:00
|
|
|
mediaUploadEvent.setGeometry(geometry.x, geometry.y)
|
|
|
|
}
|
2016-07-14 14:00:27 +02:00
|
|
|
if (chucked) {
|
2017-01-23 07:12:29 +01:00
|
|
|
resp = uploadMediaChucked(upload, body.body, ownerIds)
|
2016-07-14 14:00:27 +02:00
|
|
|
} else {
|
2017-01-23 07:12:29 +01:00
|
|
|
resp = upload.uploadMedia(body.body, ownerIds)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2016-10-05 15:12:33 +02:00
|
|
|
mediaUploadEvent.markEnd()
|
|
|
|
HotMobiLogger.getInstance(context).log(mediaUploadEvent)
|
2016-07-14 14:00:27 +02:00
|
|
|
} catch (e: IOException) {
|
|
|
|
throw UploadException(e)
|
|
|
|
} catch (e: MicroBlogException) {
|
|
|
|
throw UploadException(e)
|
|
|
|
} finally {
|
2017-01-23 07:12:29 +01:00
|
|
|
Utils.closeSilently(body)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2017-01-23 07:12:29 +01:00
|
|
|
body?.deleteOnSuccess?.addAllTo(deleteOnSuccess)
|
2017-01-23 07:41:36 +01:00
|
|
|
body?.deleteAlways?.addAllTo(deleteAlways)
|
2016-09-17 15:44:53 +02:00
|
|
|
if (media.alt_text?.isNotEmpty() ?: false) {
|
|
|
|
try {
|
|
|
|
upload.createMetadata(NewMediaMetadata(resp.id, media.alt_text))
|
|
|
|
} catch (e: MicroBlogException) {
|
|
|
|
// Ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return@mapIndexed resp.id
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2017-01-23 07:41:36 +01:00
|
|
|
return SharedMediaUploadResult(mediaIds.toTypedArray(), deleteOnSuccess, deleteAlways)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Throws(IOException::class, MicroBlogException::class)
|
|
|
|
private fun uploadMediaChucked(upload: TwitterUpload, body: Body,
|
|
|
|
ownerIds: Array<String>): 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)
|
2016-08-09 09:48:16 +02:00
|
|
|
var info: MediaUploadResponse.ProcessingInfo? = response.processingInfo
|
|
|
|
while (info != null && shouldWaitForProcess(info)) {
|
|
|
|
val checkAfterSecs = info.checkAfterSecs
|
|
|
|
if (checkAfterSecs <= 0) {
|
|
|
|
break
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2016-08-09 09:48:16 +02:00
|
|
|
try {
|
|
|
|
Thread.sleep(TimeUnit.SECONDS.toMillis(checkAfterSecs))
|
|
|
|
} catch (e: InterruptedException) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
response = upload.getUploadMediaStatus(response.id)
|
|
|
|
info = response.processingInfo
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2016-07-16 12:35:54 +02:00
|
|
|
private fun shouldWaitForProcess(info: MediaUploadResponse.ProcessingInfo): Boolean {
|
2016-07-14 14:00:27 +02:00
|
|
|
when (info.state) {
|
|
|
|
MediaUploadResponse.ProcessingInfo.State.PENDING, MediaUploadResponse.ProcessingInfo.State.IN_PROGRESS -> return true
|
|
|
|
else -> return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
private fun saveDraft(@Draft.Action draftAction: String?, statusUpdate: ParcelableStatusUpdate): Long {
|
2017-02-07 15:55:36 +01:00
|
|
|
return saveDraft(context, draftAction) {
|
|
|
|
this.unique_id = statusUpdate.draft_unique_id ?: UUID.randomUUID().toString()
|
|
|
|
this.account_keys = statusUpdate.accounts.map { it.key }.toTypedArray()
|
|
|
|
this.action_type = draftAction ?: Draft.Action.UPDATE_STATUS
|
|
|
|
this.text = statusUpdate.text
|
|
|
|
this.location = statusUpdate.location
|
|
|
|
this.media = statusUpdate.media
|
|
|
|
this.timestamp = System.currentTimeMillis()
|
|
|
|
this.action_extras = UpdateStatusActionExtras().apply {
|
|
|
|
inReplyToStatus = statusUpdate.in_reply_to_status
|
|
|
|
isPossiblySensitive = statusUpdate.is_possibly_sensitive
|
|
|
|
isRepostStatusId = statusUpdate.repost_status_id
|
|
|
|
displayCoordinates = statusUpdate.display_coordinates
|
|
|
|
attachmentUrl = statusUpdate.attachment_url
|
|
|
|
}
|
|
|
|
}
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
internal class PendingStatusUpdate(val length: Int, defaultText: String) {
|
|
|
|
|
2017-01-23 07:12:29 +01:00
|
|
|
constructor(statusUpdate: ParcelableStatusUpdate) : this(statusUpdate.accounts.size,
|
|
|
|
statusUpdate.text)
|
|
|
|
|
2016-07-14 14:00:27 +02:00
|
|
|
var sharedMediaIds: Array<String>? = null
|
|
|
|
var sharedMediaOwners: Array<UserKey>? = null
|
|
|
|
|
2017-01-23 07:12:29 +01:00
|
|
|
val overrideTexts: Array<String> = Array(length) { idx ->
|
|
|
|
defaultText
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2017-01-23 07:12:29 +01:00
|
|
|
val mediaIds: Array<Array<String>?> = arrayOfNulls(length)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-01-23 07:12:29 +01:00
|
|
|
val mediaUploadResults: Array<MediaUploadResult?> = arrayOfNulls(length)
|
|
|
|
val statusShortenResults: Array<StatusShortenResult?> = arrayOfNulls(length)
|
|
|
|
|
|
|
|
val deleteOnSuccess: ArrayList<MediaDeletionItem> = arrayListOf()
|
2017-01-23 07:41:36 +01:00
|
|
|
val deleteAlways: ArrayList<MediaDeletionItem> = arrayListOf()
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
class UpdateStatusResult {
|
|
|
|
val statuses: Array<ParcelableStatus?>
|
|
|
|
val exceptions: Array<MicroBlogException?>
|
2017-01-03 12:59:38 +01:00
|
|
|
val accountTypes: Array<String?>
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
val exception: UpdateStatusException?
|
2016-08-17 05:40:15 +02:00
|
|
|
val draftId: Long
|
|
|
|
|
2017-01-24 12:10:24 +01:00
|
|
|
val succeed: Boolean get() = exception == null && statuses.isNotEmpty() && statuses.none { it == null }
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-01-03 12:59:38 +01:00
|
|
|
constructor(count: Int, draftId: Long) {
|
|
|
|
this.statuses = arrayOfNulls(count)
|
|
|
|
this.exceptions = arrayOfNulls(count)
|
|
|
|
this.accountTypes = arrayOfNulls(count)
|
2016-07-14 14:00:27 +02:00
|
|
|
this.exception = null
|
2016-08-17 05:40:15 +02:00
|
|
|
this.draftId = draftId
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(exception: UpdateStatusException, draftId: Long) {
|
2016-07-14 14:00:27 +02:00
|
|
|
this.exception = exception
|
2017-01-03 12:59:38 +01:00
|
|
|
this.statuses = arrayOfNulls(0)
|
|
|
|
this.exceptions = arrayOfNulls(0)
|
|
|
|
this.accountTypes = arrayOfNulls(0)
|
2016-08-17 05:40:15 +02:00
|
|
|
this.draftId = draftId
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
open class UpdateStatusException : Exception {
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor() : super()
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(throwable: Throwable) : super(throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(message: String) : super(message)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class UploaderNotFoundException : UpdateStatusException {
|
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor() : super()
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(throwable: Throwable) : super(throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2016-08-17 05:40:15 +02:00
|
|
|
constructor(message: String) : super(message)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class UploadException : UpdateStatusException {
|
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor() : super()
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor(throwable: Throwable) : super(throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor(message: String) : super(message)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
class ExtensionVersionMismatchException : AbsServiceInterface.CheckServiceException()
|
|
|
|
|
|
|
|
class ShortenerNotFoundException : UpdateStatusException()
|
|
|
|
|
|
|
|
class ShortenException : UpdateStatusException {
|
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor() : super()
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor(throwable: Throwable) : super(throwable)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
2017-02-04 08:39:08 +01:00
|
|
|
constructor(message: String) : super(message)
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface StateCallback {
|
|
|
|
@WorkerThread
|
|
|
|
fun onStartUploadingMedia()
|
|
|
|
|
|
|
|
@WorkerThread
|
|
|
|
fun onUploadingProgressChanged(index: Int, current: Long, total: Long)
|
|
|
|
|
|
|
|
@WorkerThread
|
|
|
|
fun onShorteningStatus()
|
|
|
|
|
|
|
|
@WorkerThread
|
|
|
|
fun onUpdatingStatus()
|
|
|
|
|
|
|
|
@UiThread
|
2016-12-15 01:13:09 +01:00
|
|
|
fun afterExecute(result: UpdateStatusResult)
|
2016-07-14 14:00:27 +02:00
|
|
|
|
|
|
|
@UiThread
|
|
|
|
fun beforeExecute()
|
|
|
|
}
|
|
|
|
|
2017-01-23 07:12:29 +01:00
|
|
|
data class SizeLimit(
|
2017-01-23 14:31:14 +01:00
|
|
|
val image: AccountExtras.ImageLimit,
|
|
|
|
val video: AccountExtras.VideoLimit
|
2017-01-23 07:12:29 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
data class MediaStreamBody(
|
|
|
|
val body: Body,
|
|
|
|
val geometry: Point?,
|
2017-01-23 07:41:36 +01:00
|
|
|
val deleteOnSuccess: List<MediaDeletionItem>?,
|
|
|
|
val deleteAlways: List<MediaDeletionItem>?
|
2017-01-23 07:12:29 +01:00
|
|
|
) : Closeable {
|
|
|
|
override fun close() {
|
|
|
|
body.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
interface MediaDeletionItem {
|
|
|
|
fun delete(context: Context): Boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
data class UriMediaDeletionItem(val uri: Uri) : MediaDeletionItem {
|
|
|
|
override fun delete(context: Context): Boolean {
|
2017-01-26 16:15:05 +01:00
|
|
|
return Utils.deleteMedia(context, uri)
|
2017-01-23 07:12:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data class FileMediaDeletionItem(val file: File) : MediaDeletionItem {
|
|
|
|
override fun delete(context: Context): Boolean {
|
|
|
|
return file.delete()
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2017-01-23 06:27:29 +01:00
|
|
|
|
2017-01-23 07:41:36 +01:00
|
|
|
internal data class SharedMediaUploadResult(
|
|
|
|
val ids: Array<String>,
|
|
|
|
val deleteOnSuccess: List<MediaDeletionItem>,
|
|
|
|
val deleteAlways: List<MediaDeletionItem>
|
|
|
|
)
|
|
|
|
|
2016-07-14 14:00:27 +02:00
|
|
|
companion object {
|
|
|
|
|
|
|
|
private val BULK_SIZE = 256 * 1024// 128 Kib
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
2017-01-23 06:27:29 +01:00
|
|
|
fun getBodyFromMedia(
|
|
|
|
context: Context,
|
|
|
|
mediaLoader: MediaLoaderWrapper,
|
|
|
|
mediaUri: Uri,
|
|
|
|
sizeLimit: SizeLimit? = null,
|
|
|
|
@ParcelableMedia.Type type: Int,
|
2017-01-24 12:10:24 +01:00
|
|
|
chucked: Boolean,
|
2017-01-23 06:27:29 +01:00
|
|
|
readListener: ContentLengthInputStream.ReadListener
|
2017-01-23 07:12:29 +01:00
|
|
|
): MediaStreamBody {
|
2016-09-01 09:04:54 +02:00
|
|
|
val resolver = context.contentResolver
|
2017-01-23 06:27:29 +01:00
|
|
|
val mediaType = resolver.getType(mediaUri) ?: run {
|
2017-01-22 19:37:21 +01:00
|
|
|
if (mediaUri.scheme == ContentResolver.SCHEME_FILE) {
|
|
|
|
mediaUri.lastPathSegment?.substringAfterLast(".")?.let { ext ->
|
|
|
|
return@run MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return@run null
|
|
|
|
}
|
2017-01-23 14:31:14 +01:00
|
|
|
|
|
|
|
val data = if (sizeLimit != null) when (type) {
|
|
|
|
ParcelableMedia.Type.IMAGE -> imageStream(context, resolver, mediaLoader, mediaUri,
|
|
|
|
mediaType, sizeLimit)
|
|
|
|
ParcelableMedia.Type.VIDEO -> videoStream(context, resolver, mediaUri, mediaType,
|
2017-01-24 12:10:24 +01:00
|
|
|
sizeLimit, chucked)
|
2017-01-23 14:31:14 +01:00
|
|
|
else -> null
|
|
|
|
} else null
|
|
|
|
|
2017-01-23 06:27:29 +01:00
|
|
|
val cis = data?.stream ?: run {
|
2016-07-31 08:41:07 +02:00
|
|
|
val st = resolver.openInputStream(mediaUri) ?: throw FileNotFoundException(mediaUri.toString())
|
2016-09-01 09:04:54 +02:00
|
|
|
val length = st.available().toLong()
|
2017-01-23 07:12:29 +01:00
|
|
|
return@run ContentLengthInputStream(st, length)
|
2016-07-16 09:27:46 +02:00
|
|
|
}
|
2016-07-14 14:00:27 +02:00
|
|
|
cis.setReadListener(readListener)
|
2017-01-23 07:12:29 +01:00
|
|
|
val mimeType = data?.type ?: mediaType ?: "application/octet-stream"
|
|
|
|
val body = FileBody(cis, "attachment", cis.length(), ContentType.parse(mimeType))
|
2017-01-23 07:41:36 +01:00
|
|
|
val deleteOnSuccess: MutableList<MediaDeletionItem> = mutableListOf(UriMediaDeletionItem(mediaUri))
|
|
|
|
val deleteAlways: MutableList<MediaDeletionItem> = mutableListOf()
|
|
|
|
data?.deleteOnSuccess?.addAllTo(deleteOnSuccess)
|
|
|
|
data?.deleteAlways?.addAllTo(deleteAlways)
|
|
|
|
return MediaStreamBody(body, data?.geometry, deleteOnSuccess, deleteAlways)
|
2017-01-23 06:27:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private fun imageStream(
|
2017-01-23 07:12:29 +01:00
|
|
|
context: Context,
|
2017-01-23 06:27:29 +01:00
|
|
|
resolver: ContentResolver,
|
|
|
|
mediaLoader: MediaLoaderWrapper,
|
|
|
|
mediaUri: Uri,
|
|
|
|
defaultType: String?,
|
|
|
|
sizeLimit: SizeLimit
|
|
|
|
): MediaStreamData? {
|
|
|
|
var mediaType = defaultType
|
|
|
|
val o = BitmapFactory.Options()
|
|
|
|
o.inJustDecodeBounds = true
|
|
|
|
BitmapFactoryUtils.decodeUri(resolver, mediaUri, null, o)
|
|
|
|
if (o.outMimeType != null) {
|
|
|
|
mediaType = o.outMimeType
|
|
|
|
}
|
|
|
|
val size = Point(o.outWidth, o.outHeight)
|
2017-01-23 14:31:14 +01:00
|
|
|
val imageLimit = sizeLimit.image
|
2017-01-23 06:27:29 +01:00
|
|
|
o.inSampleSize = Utils.calculateInSampleSize(o.outWidth, o.outHeight,
|
2017-01-23 14:31:14 +01:00
|
|
|
imageLimit.maxWidth, imageLimit.maxHeight)
|
2017-01-23 06:27:29 +01:00
|
|
|
o.inJustDecodeBounds = false
|
|
|
|
if (o.outWidth > 0 && o.outHeight > 0 && mediaType != "image/gif") {
|
|
|
|
val displayOptions = DisplayImageOptions.Builder()
|
|
|
|
.considerExifParams(true)
|
|
|
|
.build()
|
|
|
|
val bitmap = mediaLoader.loadImageSync(mediaUri.toString(),
|
|
|
|
ImageSize(o.outWidth, o.outHeight).scaleDown(o.inSampleSize),
|
|
|
|
displayOptions)
|
|
|
|
|
|
|
|
if (bitmap != null) {
|
|
|
|
size.set(bitmap.width, bitmap.height)
|
2017-01-23 07:12:29 +01:00
|
|
|
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mediaType)
|
|
|
|
val tempFile = File.createTempFile("twidere__scaled_image_", ".$ext", context.cacheDir)
|
|
|
|
tempFile.outputStream().use { os ->
|
|
|
|
when (mediaType) {
|
|
|
|
"image/png", "image/x-png", "image/webp", "image-x-webp" -> {
|
|
|
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, os)
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os)
|
|
|
|
}
|
2017-01-23 06:27:29 +01:00
|
|
|
}
|
|
|
|
}
|
2017-01-23 07:12:29 +01:00
|
|
|
return MediaStreamData(ContentLengthInputStream(tempFile), mediaType, size,
|
2017-01-23 07:41:36 +01:00
|
|
|
null, listOf(FileMediaDeletionItem(tempFile)))
|
2017-01-23 06:27:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2016-07-16 09:27:46 +02:00
|
|
|
|
2017-01-23 06:27:29 +01:00
|
|
|
private fun videoStream(
|
|
|
|
context: Context,
|
|
|
|
resolver: ContentResolver,
|
|
|
|
mediaUri: Uri,
|
2017-01-23 14:31:14 +01:00
|
|
|
defaultType: String?,
|
2017-01-24 12:10:24 +01:00
|
|
|
sizeLimit: SizeLimit,
|
|
|
|
chucked: Boolean
|
2017-01-23 06:27:29 +01:00
|
|
|
): MediaStreamData? {
|
2017-01-23 14:31:14 +01:00
|
|
|
var mediaType = defaultType
|
|
|
|
val videoLimit = sizeLimit.video
|
|
|
|
val geometry = Point()
|
|
|
|
var duration = -1L
|
|
|
|
var framerate = -1.0
|
2017-01-24 12:10:24 +01:00
|
|
|
var size = -1L
|
2017-01-23 14:31:14 +01:00
|
|
|
// TODO only transcode video if needed, use `MediaMetadataRetriever`
|
|
|
|
val retriever = MediaMetadataRetriever()
|
|
|
|
try {
|
|
|
|
retriever.setDataSource(context, mediaUri)
|
|
|
|
val extractedMimeType = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_MIMETYPE)
|
|
|
|
if (extractedMimeType != null) {
|
|
|
|
mediaType = extractedMimeType
|
|
|
|
}
|
|
|
|
geometry.x = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toInt(-1)
|
|
|
|
geometry.y = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toInt(-1)
|
|
|
|
duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLong(-1)
|
|
|
|
framerate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE).toDouble(-1.0)
|
2017-01-24 12:10:24 +01:00
|
|
|
|
|
|
|
size = resolver.openFileDescriptor(mediaUri, "r").use { it.statSize }
|
2017-01-23 14:31:14 +01:00
|
|
|
} catch (e: Exception) {
|
2017-01-24 12:10:24 +01:00
|
|
|
DebugLog.w(LOGTAG, "Unable to retrieve video info", e)
|
2017-01-23 14:31:14 +01:00
|
|
|
} finally {
|
2017-01-24 12:10:24 +01:00
|
|
|
retriever.releaseSafe()
|
2017-01-23 14:31:14 +01:00
|
|
|
}
|
|
|
|
|
2017-01-24 14:40:59 +01:00
|
|
|
if (geometry.x > 0 && geometry.y > 0 && videoLimit.checkGeometry(geometry.x, geometry.y)
|
|
|
|
&& framerate > 0 && videoLimit.checkFrameRate(framerate)
|
|
|
|
&& size > 0 && videoLimit.checkSize(size, chucked)) {
|
2017-01-23 14:31:14 +01:00
|
|
|
// Size valid, upload directly
|
2017-01-24 12:10:24 +01:00
|
|
|
DebugLog.d(LOGTAG, "Upload video directly")
|
2017-01-23 14:31:14 +01:00
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
2017-01-24 12:10:24 +01:00
|
|
|
if (!videoLimit.checkMinDuration(duration, chucked)) {
|
|
|
|
throw UploadException(context.getString(R.string.message_video_too_short))
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!videoLimit.checkMaxDuration(duration, chucked)) {
|
|
|
|
throw UploadException(context.getString(R.string.message_video_too_long))
|
|
|
|
}
|
|
|
|
|
2017-01-23 06:27:29 +01:00
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
2017-01-24 12:10:24 +01:00
|
|
|
// Go get a new phone
|
2017-01-23 06:27:29 +01:00
|
|
|
return null
|
|
|
|
}
|
2017-01-24 12:10:24 +01:00
|
|
|
DebugLog.d(LOGTAG, "Transcoding video")
|
|
|
|
|
2017-01-23 06:27:29 +01:00
|
|
|
val ext = mediaUri.lastPathSegment.substringAfterLast(".")
|
|
|
|
val strategy = MediaFormatStrategyPresets.createAndroid720pStrategy()
|
|
|
|
val listener = object : MediaTranscoder.Listener {
|
|
|
|
override fun onTranscodeFailed(exception: Exception?) {
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onTranscodeCompleted() {
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onTranscodeProgress(progress: Double) {
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onTranscodeCanceled() {
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2017-01-24 12:10:24 +01:00
|
|
|
val pfd = resolver.openFileDescriptor(mediaUri, "r")
|
2017-01-23 06:27:29 +01:00
|
|
|
val tempFile = File.createTempFile("twidere__encoded_video_", ".$ext", context.cacheDir)
|
|
|
|
val future = MediaTranscoder.getInstance().transcodeVideo(pfd.fileDescriptor,
|
|
|
|
tempFile.absolutePath, strategy, listener)
|
|
|
|
try {
|
|
|
|
future.get()
|
|
|
|
} catch (e: Exception) {
|
2017-01-24 12:10:24 +01:00
|
|
|
DebugLog.w(LOGTAG, "Error transcoding video, try upload directly", e)
|
2017-01-23 06:27:29 +01:00
|
|
|
tempFile.delete()
|
|
|
|
return null
|
|
|
|
}
|
2017-01-23 14:31:14 +01:00
|
|
|
return MediaStreamData(ContentLengthInputStream(tempFile.inputStream(), tempFile.length()),
|
|
|
|
mediaType, geometry, null, listOf(FileMediaDeletionItem(tempFile)))
|
2017-01-23 06:27:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
internal class MediaStreamData(
|
2017-01-23 07:12:29 +01:00
|
|
|
val stream: ContentLengthInputStream?,
|
|
|
|
val type: String?,
|
|
|
|
val geometry: Point?,
|
2017-01-23 07:41:36 +01:00
|
|
|
val deleteOnSuccess: List<MediaDeletionItem>?,
|
|
|
|
val deleteAlways: List<MediaDeletionItem>?
|
2017-01-23 06:27:29 +01:00
|
|
|
)
|
|
|
|
|
2017-02-07 15:55:36 +01:00
|
|
|
fun saveDraft(context: Context, @Draft.Action action: String?, config: Draft.() -> Unit): Long {
|
|
|
|
val draft = Draft()
|
|
|
|
draft.action_type = action
|
|
|
|
draft.timestamp = System.currentTimeMillis()
|
|
|
|
config(draft)
|
|
|
|
val resolver = context.contentResolver
|
|
|
|
val draftUri = resolver.insert(Drafts.CONTENT_URI, DraftValuesCreator.create(draft)) ?: return -1
|
|
|
|
return NumberUtils.toLong(draftUri.lastPathSegment, -1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun deleteDraft(context: Context, id: Long) {
|
|
|
|
val where = Expression.equalsArgs(Drafts._ID).sql
|
|
|
|
val whereArgs = arrayOf(id.toString())
|
|
|
|
context.contentResolver.delete(Drafts.CONTENT_URI, where, whereArgs)
|
|
|
|
}
|
2016-07-16 09:27:46 +02:00
|
|
|
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2017-01-21 17:33:20 +01:00
|
|
|
|
2016-07-14 14:00:27 +02:00
|
|
|
}
|
2017-01-24 12:10:24 +01:00
|
|
|
|