Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt

1081 lines
46 KiB
Kotlin
Raw Normal View History

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.support.media.ExifInterface
2016-07-14 14:00:27 +02:00
import android.text.TextUtils
2017-01-22 19:37:21 +01:00
import android.webkit.MimeTypeMap
import com.twitter.Validator
2017-01-23 06:27:29 +01:00
import net.ypresto.androidtranscoder.MediaTranscoder
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets
2017-01-24 12:10:24 +01:00
import org.mariotaku.ktextension.*
2017-03-05 09:08:09 +01:00
import org.mariotaku.library.objectcursor.ObjectCursor
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
2017-04-19 14:05:56 +02:00
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.mastodon.model.Attachment
2016-07-14 14:00:27 +02:00
import org.mariotaku.microblog.library.twitter.TwitterUpload
2017-04-19 14:05:56 +02:00
import org.mariotaku.microblog.library.twitter.model.ErrorInfo
import org.mariotaku.microblog.library.twitter.model.MediaUploadResponse
import org.mariotaku.microblog.library.twitter.model.NewMediaMetadata
import org.mariotaku.microblog.library.twitter.model.StatusUpdate
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.*
2017-04-21 14:24:15 +02:00
import org.mariotaku.twidere.alias.MastodonStatusUpdate
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
2017-04-19 14:05:56 +02:00
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
2017-04-19 08:50:47 +02:00
import org.mariotaku.twidere.extension.model.api.toParcelable
2017-04-14 09:05:51 +02:00
import org.mariotaku.twidere.extension.model.applyUpdateStatus
2017-02-28 08:34:00 +01:00
import org.mariotaku.twidere.extension.model.mediaSizeLimit
2017-03-01 15:12:25 +01:00
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
2017-02-28 08:34:00 +01:00
import org.mariotaku.twidere.extension.model.textLimit
import org.mariotaku.twidere.extension.text.twitter.getTweetLength
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
2017-03-25 16:53:49 +01:00
import org.mariotaku.twidere.model.schedule.ScheduleInfo
2016-07-14 14:00:27 +02:00
import org.mariotaku.twidere.model.util.ParcelableLocationUtils
2017-05-13 08:19:23 +02:00
import org.mariotaku.twidere.preference.ComponentPickerPreference
2016-07-14 14:00:27 +02:00
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-03-28 05:15:58 +02:00
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
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
/**
2017-04-12 08:12:45 +02:00
* Update status
*
2016-07-14 14:00:27 +02:00
* 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-03-25 16:53:49 +01:00
) : BaseAbstractTask<Pair<ParcelableStatusUpdate, ScheduleInfo?>, UpdateStatusTask.UpdateStatusResult, Any?>(context) {
2016-07-14 14:00:27 +02:00
2017-03-25 16:53:49 +01:00
override fun doLongOperation(params: Pair<ParcelableStatusUpdate, ScheduleInfo?>): UpdateStatusResult {
val (update, info) = params
2017-04-14 09:57:25 +02:00
val draftId = saveDraft(context, update.draft_action ?: Draft.Action.UPDATE_STATUS) {
applyUpdateStatus(update)
}
2017-02-07 15:55:36 +01:00
microBlogWrapper.addSendingDraftId(draftId)
2016-07-14 14:00:27 +02:00
try {
2017-03-25 16:53:49 +01:00
val result = doUpdateStatus(update, info, draftId)
deleteOrUpdateDraft(update, result, draftId)
2016-07-14 14:00:27 +02:00
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()
}
2017-03-27 05:08:09 +02:00
override fun afterExecute(callback: Any?, result: UpdateStatusResult) {
2016-12-15 01:13:09 +01:00
stateCallback.afterExecute(result)
2017-03-25 16:53:49 +01:00
logUpdateStatus(params.first, result)
2017-01-03 12:59:38 +01:00
}
private fun logUpdateStatus(statusUpdate: ParcelableStatusUpdate, result: UpdateStatusResult) {
2017-01-03 12:59:38 +01:00
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(), statusUpdate.draft_action,
mediaType, hasLocation, preciseLocation, result.succeed,
result.exceptions.firstOrNull() ?: result.exception))
2016-07-14 14:00:27 +02:00
}
@Throws(UpdateStatusException::class)
2017-03-25 16:53:49 +01:00
private fun doUpdateStatus(update: ParcelableStatusUpdate, info: ScheduleInfo?, draftId: Long):
UpdateStatusResult {
2016-07-14 14:00:27 +02:00
val app = TwidereApplication.getInstance(context)
val uploader = getMediaUploader(app)
val shortener = getStatusShortener(app)
val pendingUpdate = PendingStatusUpdate(update)
2016-07-14 14:00:27 +02:00
val result: UpdateStatusResult
try {
2017-03-25 16:53:49 +01:00
uploadMedia(uploader, update, info, pendingUpdate)
2017-01-23 07:41:36 +01:00
shortenStatus(shortener, update, pendingUpdate)
2017-03-25 16:53:49 +01:00
if (info != null) {
result = requestScheduleStatus(update, pendingUpdate, info, draftId)
} else {
result = requestUpdateStatus(update, pendingUpdate, 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:41:36 +01:00
// Cleanup
pendingUpdate.deleteOnSuccess.forEach { item -> item.delete(context) }
2017-02-26 14:49:31 +01:00
} catch (e: UploadException) {
e.deleteAlways?.forEach { it.delete(context) }
throw e
2017-01-23 07:41:36 +01:00
} finally {
// Cleanup
pendingUpdate.deleteAlways.forEach { item -> item.delete(context) }
2017-02-04 11:42:14 +01:00
uploader?.unbindService()
shortener?.unbindService()
}
2016-07-14 14:00:27 +02:00
return result
}
2017-03-25 16:53:49 +01:00
private fun deleteOrUpdateDraft(update: ParcelableStatusUpdate, result: UpdateStatusResult,
draftId: Long) {
2017-04-10 11:35:35 +02:00
val where = Expression.equals(Drafts._ID, draftId).sql
2016-07-14 14:00:27 +02:00
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(","))
2017-04-10 11:35:35 +02:00
cr.update(Drafts.CONTENT_URI, values, where, null)
2016-07-14 14:00:27 +02:00
// TODO show error message
} else {
2017-04-10 11:35:35 +02:00
cr.delete(Drafts.CONTENT_URI, where, null)
2016-07-14 14:00:27 +02:00
}
}
@Throws(UploadException::class)
private fun uploadMedia(uploader: MediaUploaderInterface?,
update: ParcelableStatusUpdate,
2017-03-25 16:53:49 +01:00
info: ScheduleInfo?,
pendingUpdate: PendingStatusUpdate) {
2016-07-14 14:00:27 +02:00
stateCallback.onStartUploadingMedia()
2017-03-25 16:53:49 +01:00
if (uploader != null) {
2016-07-14 14:00:27 +02:00
uploadMediaWithExtension(uploader, update, pendingUpdate)
2017-03-25 16:53:49 +01:00
} else if (info == null) {
uploadMediaWithDefaultProvider(update, pendingUpdate)
2016-07-14 14:00:27 +02:00
}
}
@Throws(UploadException::class)
private fun uploadMediaWithExtension(uploader: MediaUploaderInterface,
update: ParcelableStatusUpdate,
pending: PendingStatusUpdate) {
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
2017-04-20 14:04:37 +02:00
pending.overrideTexts[i] = "${pending.overrideTexts[i]} ${uploadResult.media_uris.joinToString(" ")}"
2016-07-14 14:00:27 +02:00
}
}
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
val validator = Validator()
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]
2017-02-28 08:34:00 +01:00
val textLimit = account.textLimit
val ignoreMentions = account.type == AccountType.TWITTER
if (textLimit >= 0 && validator.getTweetLength(text, ignoreMentions,
2017-04-16 10:47:02 +02:00
update.in_reply_to_status, account.key) <= textLimit) {
2016-12-08 08:47:17 +01:00
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
}
}
2017-03-25 16:53:49 +01:00
@Throws(UpdateStatusException::class)
private fun requestScheduleStatus(
statusUpdate: ParcelableStatusUpdate,
pendingUpdate: PendingStatusUpdate,
scheduleInfo: ScheduleInfo,
draftId: Long
): UpdateStatusResult {
stateCallback.onUpdatingStatus()
2017-03-28 05:15:58 +02:00
if (!extraFeaturesService.isEnabled(ExtraFeaturesService.FEATURE_SCHEDULE_STATUS)) {
throw SchedulerNotFoundException(context.getString(R.string.error_message_scheduler_not_available))
}
2017-03-25 16:53:49 +01:00
val controller = scheduleProvider ?: run {
2017-03-28 05:15:58 +02:00
throw SchedulerNotFoundException(context.getString(R.string.error_message_scheduler_not_available))
2017-03-25 16:53:49 +01:00
}
controller.scheduleStatus(statusUpdate, pendingUpdate, scheduleInfo)
2017-03-25 16:53:49 +01:00
return UpdateStatusResult(pendingUpdate.length, draftId)
}
@Throws(UpdateStatusException::class)
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
2017-04-19 14:05:56 +02:00
2016-07-14 14:00:27 +02:00
try {
2017-04-19 14:05:56 +02:00
val status = when (account.type) {
2016-12-03 06:48:40 +01:00
AccountType.FANFOU -> {
2017-04-19 14:05:56 +02:00
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
2016-07-14 14:00:27 +02:00
// Call uploadPhoto if media present
if (statusUpdate.media.isNotNullOrEmpty()) {
2016-07-14 14:00:27 +02:00
// Fanfou only allow one photo
fanfouUpdateStatusWithPhoto(microBlog, statusUpdate, pendingUpdate,
2017-04-12 08:12:45 +02:00
account.mediaSizeLimit, i)
2016-07-14 14:00:27 +02:00
} else {
twitterUpdateStatus(microBlog, statusUpdate, pendingUpdate, i)
2016-07-14 14:00:27 +02:00
}
}
2017-04-19 14:05:56 +02:00
AccountType.MASTODON -> {
val mastodon = account.newMicroBlogInstance(context, Mastodon::class.java)
mastodonUpdateStatus(mastodon, statusUpdate, pendingUpdate, i)
}
2016-07-14 14:00:27 +02:00
else -> {
2017-04-19 14:05:56 +02:00
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
twitterUpdateStatus(microBlog, statusUpdate, pendingUpdate, i)
2016-07-14 14:00:27 +02:00
}
}
2017-04-19 14:05:56 +02:00
result.statuses[i] = status
2016-07-14 14:00:27 +02:00
} catch (e: MicroBlogException) {
result.exceptions[i] = e
}
}
return result
}
@Throws(MicroBlogException::class, UploadException::class)
private fun fanfouUpdateStatusWithPhoto(microBlog: MicroBlog, statusUpdate: ParcelableStatusUpdate,
2017-04-19 14:05:56 +02:00
pendingUpdate: PendingStatusUpdate, sizeLimit: SizeLimit?, updateIndex: Int): ParcelableStatus {
if (statusUpdate.media.size > 1) {
throw MicroBlogException(context.getString(R.string.error_too_many_photos_fanfou))
}
val media = statusUpdate.media.first()
try {
2017-04-19 14:05:56 +02:00
val details = statusUpdate.accounts[updateIndex]
return getBodyFromMedia(context, media, sizeLimit, false, ContentLengthInputStream.ReadListener { length, position ->
stateCallback.onUploadingProgressChanged(-1, position, length)
2017-04-12 08:12:45 +02:00
}).use { (body) ->
val photoUpdate = PhotoStatusUpdate(body, pendingUpdate.overrideTexts[updateIndex])
return@use microBlog.uploadPhoto(photoUpdate)
}.toParcelable(details)
} catch (e: IOException) {
throw UploadException(e)
}
}
2016-07-14 14:00:27 +02:00
/**
* 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
2017-04-07 06:12:34 +02:00
if (update.media.isNullOrEmpty()) 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 {
val (ids, deleteOnSuccess, deleteAlways) = uploadMicroBlogMediaShared(context,
2017-05-11 07:01:01 +02:00
upload, account, update.media, null, ownerIds, true, stateCallback)
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)
val (ids, deleteOnSuccess, deleteAlways) = uploadMicroBlogMediaShared(context,
2017-05-11 07:01:01 +02:00
upload, account, update.media, null, ownerIds, false, stateCallback)
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
}
AccountType.MASTODON -> {
val mastodon = account.newMicroBlogInstance(context, cls = Mastodon::class.java)
val (ids, deleteOnSuccess, deleteAlways) = uploadMastodonMedia(context,
mastodon, account, update.media, false, stateCallback)
mediaIds = ids
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,
2017-04-19 14:05:56 +02:00
pendingUpdate: PendingStatusUpdate, index: Int): ParcelableStatus {
val overrideText = pendingUpdate.overrideTexts[index]
2016-07-14 14:00:27 +02:00
val status = StatusUpdate(overrideText)
val inReplyToStatus = statusUpdate.in_reply_to_status
2017-04-14 06:19:21 +02:00
2017-04-19 14:05:56 +02:00
val details = statusUpdate.accounts[index]
2017-04-17 11:28:22 +02:00
if (statusUpdate.draft_action == Draft.Action.REPLY && inReplyToStatus != null) {
status.inReplyToStatusId(inReplyToStatus.id)
2017-04-14 06:19:21 +02:00
if (details.type == AccountType.TWITTER && statusUpdate.extended_reply_mode) {
status.autoPopulateReplyMetadata(true)
2017-04-16 10:47:02 +02:00
if (statusUpdate.excluded_reply_user_ids.isNotNullOrEmpty()) {
status.excludeReplyUserIds(statusUpdate.excluded_reply_user_ids)
}
}
2016-07-14 14:00:27 +02:00
}
if (statusUpdate.repost_status_id != null) {
2017-04-02 09:12:37 +02:00
status.repostStatusId(statusUpdate.repost_status_id)
2016-07-14 14:00:27 +02:00
}
if (statusUpdate.attachment_url != null) {
2017-04-02 09:12:37 +02:00
status.attachmentUrl(statusUpdate.attachment_url)
2016-07-14 14:00:27 +02:00
}
if (statusUpdate.location != null) {
status.location(ParcelableLocationUtils.toGeoLocation(statusUpdate.location))
status.displayCoordinates(statusUpdate.display_coordinates)
}
val mediaIds = pendingUpdate.mediaIds[index]
if (mediaIds != null) {
2017-04-02 09:12:37 +02:00
status.mediaIds(mediaIds)
2016-07-14 14:00:27 +02:00
}
2017-01-22 14:31:25 +01:00
if (statusUpdate.is_possibly_sensitive) {
status.possiblySensitive(statusUpdate.is_possibly_sensitive)
}
return microBlog.updateStatus(status).toParcelable(details)
2017-04-19 14:05:56 +02:00
}
@Throws(MicroBlogException::class)
private fun mastodonUpdateStatus(mastodon: Mastodon, statusUpdate: ParcelableStatusUpdate,
pendingUpdate: PendingStatusUpdate, index: Int): ParcelableStatus {
val overrideText = pendingUpdate.overrideTexts[index]
val status = MastodonStatusUpdate(overrideText)
val inReplyToStatus = statusUpdate.in_reply_to_status
val details = statusUpdate.accounts[index]
if (statusUpdate.draft_action == Draft.Action.REPLY && inReplyToStatus != null) {
status.inReplyToId(inReplyToStatus.id)
}
val mediaIds = pendingUpdate.mediaIds[index]
if (mediaIds != null) {
status.mediaIds(mediaIds)
}
if (statusUpdate.is_possibly_sensitive) {
status.sensitive(statusUpdate.is_possibly_sensitive)
}
2017-04-20 19:23:11 +02:00
if (statusUpdate.summary != null) {
status.spoilerText(statusUpdate.summary)
}
if (statusUpdate.visibility != null) {
status.visibility(statusUpdate.visibility)
}
return mastodon.postStatus(status).toParcelable(details)
2016-07-14 14:00:27 +02:00
}
2017-03-25 16:53:49 +01:00
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)
}
}
2017-03-25 16:53:49 +01:00
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)
2017-05-13 08:19:23 +02:00
if (ComponentPickerPreference.isNoneValue(shortenerComponent)) return null
2016-07-14 14:00:27 +02:00
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: 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)
2017-05-13 08:19:23 +02:00
if (ComponentPickerPreference.isNoneValue(uploaderComponent)) return null
val uploader = MediaUploaderInterface.getInstance(app, uploaderComponent) ?:
throw UploaderNotFoundException(context.getString(R.string.error_message_media_uploader_not_found))
2016-07-14 14:00:27 +02:00
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), e)
2016-07-14 14:00:27 +02:00
}
throw UploadException(e)
}
return uploader
}
private fun isDuplicate(exception: Exception): Boolean {
return exception is MicroBlogException && exception.errorCode == ErrorInfo.STATUS_IS_DUPLICATE
}
class PendingStatusUpdate internal constructor(val length: Int, defaultText: String) {
2016-07-14 14:00:27 +02:00
internal 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
val overrideTexts: Array<String> = Array(length) { defaultText }
val mediaIds: Array<Array<String>?> = arrayOfNulls(length)
2016-07-14 14:00:27 +02: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-03-25 16:53:49 +01:00
val succeed: Boolean get() = exception == null && exceptions.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 {
protected constructor() : super()
2016-07-14 14:00:27 +02:00
protected constructor(detailMessage: String, throwable: Throwable) : super(detailMessage, throwable)
2016-07-14 14:00:27 +02:00
protected constructor(throwable: Throwable) : super(throwable)
2016-07-14 14:00:27 +02:00
protected constructor(message: String) : super(message)
2016-07-14 14:00:27 +02:00
}
class UploaderNotFoundException(message: String) : UpdateStatusException(message)
2017-03-25 16:53:49 +01:00
class SchedulerNotFoundException(message: String) : UpdateStatusException(message)
2016-07-14 14:00:27 +02:00
class UploadException : UpdateStatusException {
2017-02-26 14:49:31 +01:00
var deleteAlways: List<MediaDeletionItem>? = null
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
}
2017-03-25 16:53:49 +01:00
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
}
2017-02-14 13:32:15 +01:00
interface StateCallback : UploadCallback {
2016-07-14 14:00:27 +02:00
@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-02-14 13:32:15 +01:00
interface UploadCallback {
@WorkerThread
fun onStartUploadingMedia()
@WorkerThread
fun onUploadingProgressChanged(index: Int, current: Long, total: Long)
}
data class SizeLimit(
2017-01-23 14:31:14 +01:00
val image: AccountExtras.ImageLimit,
val video: AccountExtras.VideoLimit
)
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>?
) : 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)
}
}
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-02-14 13:32:15 +01:00
data class SharedMediaUploadResult(
2017-01-23 07:41:36 +01:00
val ids: Array<String>,
val deleteOnSuccess: List<MediaDeletionItem>,
val deleteAlways: List<MediaDeletionItem>
)
2016-07-14 14:00:27 +02:00
companion object {
2017-05-11 07:01:01 +02:00
private val BULK_SIZE = 512 * 1024// 512 Kib
2016-07-14 14:00:27 +02:00
2017-02-14 13:32:15 +01:00
@Throws(UploadException::class)
fun uploadMicroBlogMediaShared(context: Context, upload: TwitterUpload,
account: AccountDetails, media: Array<ParcelableMediaUpdate>,
2017-05-11 07:01:01 +02:00
mediaCategory: String? = null, ownerIds: Array<String>?, chucked: Boolean,
callback: UploadCallback?): SharedMediaUploadResult {
2017-02-14 13:32:15 +01:00
val deleteOnSuccess = ArrayList<MediaDeletionItem>()
val deleteAlways = ArrayList<MediaDeletionItem>()
val mediaIds = media.mapIndexedToArray { index, media ->
2017-02-14 13:32:15 +01:00
val resp: MediaUploadResponse
//noinspection TryWithIdenticalCatches
var body: MediaStreamBody? = null
try {
2017-02-28 08:34:00 +01:00
val sizeLimit = account.mediaSizeLimit
body = getBodyFromMedia(context, media, sizeLimit, chucked,
ContentLengthInputStream.ReadListener { length, position ->
2017-03-05 09:08:09 +01:00
callback?.onUploadingProgressChanged(index, position, length)
})
2017-02-14 13:32:15 +01:00
if (chucked) {
2017-05-11 07:01:01 +02:00
resp = uploadMediaChucked(upload, body.body, mediaCategory, ownerIds)
2017-02-14 13:32:15 +01:00
} else {
resp = upload.uploadMedia(body.body, ownerIds)
}
} catch (e: IOException) {
2017-02-26 14:49:31 +01:00
throw UploadException(e).apply {
this.deleteAlways = deleteAlways
}
2017-02-14 13:32:15 +01:00
} catch (e: MicroBlogException) {
2017-02-26 14:49:31 +01:00
throw UploadException(e).apply {
this.deleteAlways = deleteAlways
}
2017-02-14 13:32:15 +01:00
} finally {
2017-04-17 04:44:58 +02:00
body?.close()
2017-02-14 13:32:15 +01:00
}
body?.deleteOnSuccess?.addAllTo(deleteOnSuccess)
body?.deleteAlways?.addAllTo(deleteAlways)
if (media.alt_text?.isNotEmpty() ?: false) {
try {
upload.createMetadata(NewMediaMetadata(resp.id, media.alt_text))
} catch (e: MicroBlogException) {
// Ignore
}
}
return@mapIndexedToArray resp.id
}
return SharedMediaUploadResult(mediaIds, deleteOnSuccess, deleteAlways)
}
@Throws(UploadException::class)
fun uploadMastodonMedia(context: Context, mastodon: Mastodon,
account: AccountDetails, media: Array<ParcelableMediaUpdate>,
chucked: Boolean, callback: UploadCallback?): SharedMediaUploadResult {
val deleteOnSuccess = ArrayList<MediaDeletionItem>()
val deleteAlways = ArrayList<MediaDeletionItem>()
val mediaIds = media.mapIndexedToArray { index, media ->
val resp: Attachment
//noinspection TryWithIdenticalCatches
var body: MediaStreamBody? = null
try {
val sizeLimit = account.mediaSizeLimit
body = getBodyFromMedia(context, media, sizeLimit, chucked,
ContentLengthInputStream.ReadListener { length, position ->
callback?.onUploadingProgressChanged(index, position, length)
})
resp = mastodon.uploadMediaAttachment(body.body)
} catch (e: IOException) {
throw UploadException(e).apply {
this.deleteAlways = deleteAlways
}
} catch (e: MicroBlogException) {
throw UploadException(e).apply {
this.deleteAlways = deleteAlways
}
} finally {
body?.close()
}
body?.deleteOnSuccess?.addAllTo(deleteOnSuccess)
body?.deleteAlways?.addAllTo(deleteAlways)
return@mapIndexedToArray resp.id
2017-02-14 13:32:15 +01:00
}
return SharedMediaUploadResult(mediaIds, deleteOnSuccess, deleteAlways)
2017-02-14 13:32:15 +01:00
}
2016-07-14 14:00:27 +02:00
@Throws(IOException::class)
2017-04-20 14:04:37 +02:00
fun getBodyFromMedia(context: Context, media: ParcelableMediaUpdate, sizeLimit: SizeLimit? = null,
chucked: Boolean, readListener: ContentLengthInputStream.ReadListener): MediaStreamBody {
return getBodyFromMedia(context, Uri.parse(media.uri), media.type, media.delete_always,
media.delete_on_success, sizeLimit, chucked, readListener)
}
@Throws(IOException::class)
fun getBodyFromMedia(context: Context, mediaUri: Uri, @ParcelableMedia.Type type: Int,
isDeleteAlways: Boolean, isDeleteOnSuccess: Boolean, sizeLimit: SizeLimit? = null,
chucked: Boolean, readListener: ContentLengthInputStream.ReadListener?): MediaStreamBody {
val deleteOnSuccessList: MutableList<MediaDeletionItem> = mutableListOf()
val deleteAlwaysList: MutableList<MediaDeletionItem> = mutableListOf()
val deletionItem = UriMediaDeletionItem(mediaUri)
if (isDeleteAlways) {
deleteAlwaysList.add(deletionItem)
} else if (isDeleteOnSuccess) {
deleteOnSuccessList.add(deletionItem)
}
try {
val resolver = context.contentResolver
val mediaType = resolver.getType(mediaUri) ?: run {
return@run mediaUri.lastPathSegment?.substringAfterLast(".")?.let { ext ->
MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
2017-01-22 19:37:21 +01:00
}
}
2017-01-23 14:31:14 +01:00
2017-04-20 14:04:37 +02:00
val data = when (type) {
ParcelableMedia.Type.IMAGE -> imageStream(context, resolver, mediaUri, mediaType,
sizeLimit?.image)
ParcelableMedia.Type.VIDEO -> videoStream(context, resolver, mediaUri, mediaType,
sizeLimit?.video, chucked)
else -> null
}
2017-01-23 14:31:14 +01:00
2017-04-20 14:04:37 +02:00
val cis = data?.stream ?: run {
val st = resolver.openInputStream(mediaUri) ?: throw FileNotFoundException(mediaUri.toString())
val length = st.available().toLong()
return@run ContentLengthInputStream(st, length)
}
cis.setReadListener(readListener)
val mimeType = data?.type ?: mediaType ?: "application/octet-stream"
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: ".bin"
val fileName = mediaUri.lastPathSegment?.substringBeforeLast(".") ?: "attachment"
val body = FileBody(cis, "$fileName.$extension", cis.length(), ContentType.parse(mimeType))
data?.deleteOnSuccess?.addAllTo(deleteOnSuccessList)
data?.deleteAlways?.addAllTo(deleteAlwaysList)
return MediaStreamBody(body, data?.geometry, deleteOnSuccessList, deleteAlwaysList)
} finally {
if (isDeleteAlways) {
deleteAlwaysList.forEach { it.delete(context) }
}
2017-02-26 14:49:31 +01:00
}
2017-01-23 06:27:29 +01:00
}
2017-02-14 13:32:15 +01:00
@Throws(IOException::class, MicroBlogException::class)
private fun uploadMediaChucked(upload: TwitterUpload, body: Body,
2017-05-11 07:01:01 +02:00
mediaCategory: String? = null, ownerIds: Array<String>?): MediaUploadResponse {
2017-02-14 13:32:15 +01:00
val mediaType = body.contentType().contentType
val length = body.length()
val stream = body.stream()
2017-05-11 07:01:01 +02:00
var response = upload.initUploadMedia(mediaType, length, mediaCategory, ownerIds)
2017-02-14 13:32:15 +01:00
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)
var info: MediaUploadResponse.ProcessingInfo? = response.processingInfo
while (info != null && shouldWaitForProcess(info)) {
val checkAfterSecs = info.checkAfterSecs
if (checkAfterSecs <= 0) {
break
}
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(checkAfterSecs))
} catch (e: InterruptedException) {
break
}
response = upload.getUploadMediaStatus(response.id)
info = response.processingInfo
}
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 shouldWaitForProcess(info: MediaUploadResponse.ProcessingInfo): Boolean {
when (info.state) {
MediaUploadResponse.ProcessingInfo.State.PENDING, MediaUploadResponse.ProcessingInfo.State.IN_PROGRESS -> return true
else -> return false
}
}
2017-04-08 11:00:13 +02:00
private fun imageStream(
context: Context,
resolver: ContentResolver,
mediaUri: Uri,
defaultType: String?,
imageLimit: AccountExtras.ImageLimit?
2017-04-08 11:00:13 +02:00
): MediaStreamData? {
var mediaType = defaultType
val o = BitmapFactory.Options()
o.inJustDecodeBounds = true
2017-04-17 04:44:58 +02:00
BitmapFactoryUtils.decodeUri(resolver, mediaUri, opts = o)
2017-04-08 11:00:13 +02:00
// Try to use decoded media type
if (o.outMimeType != null) {
mediaType = o.outMimeType
}
// Skip if media is GIF
if (o.outWidth <= 0 || o.outHeight <= 0 || mediaType == "image/gif") {
return null
}
if (imageLimit == null || imageLimit.checkGeomentry(o.outWidth, o.outHeight)) return null
2017-04-17 04:44:58 +02:00
o.inSampleSize = BitmapUtils.calculateInSampleSize(o.outWidth, o.outHeight,
imageLimit.maxWidth, imageLimit.maxHeight)
2017-04-08 11:00:13 +02:00
o.inJustDecodeBounds = false
// Do actual image decoding
val bitmap = context.contentResolver.openInputStream(mediaUri).use {
BitmapFactory.decodeStream(it, null, o)
} ?: return null
val size = Point(bitmap.width, bitmap.height)
val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mediaType)
val tempFile = File.createTempFile("twidere__scaled_image_", ".$ext", context.cacheDir)
when (mediaType) {
"image/png", "image/x-png", "image/webp", "image-x-webp" -> {
tempFile.outputStream().use { os ->
bitmap.compress(Bitmap.CompressFormat.PNG, 0, os)
}
}
"image/jpeg" -> {
val origExif = context.contentResolver.openInputStream(mediaUri)
.use(::ExifInterface)
tempFile.outputStream().use { os ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os)
}
val orientation = origExif.getAttribute(ExifInterface.TAG_ORIENTATION)
if (orientation != null) {
ExifInterface(tempFile.absolutePath).apply {
setAttribute(ExifInterface.TAG_ORIENTATION, orientation)
saveAttributes()
}
}
}
else -> {
tempFile.outputStream().use { os ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os)
}
}
}
return MediaStreamData(ContentLengthInputStream(tempFile), mediaType, size,
null, listOf(FileMediaDeletionItem(tempFile)))
}
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?,
videoLimit: AccountExtras.VideoLimit?,
2017-01-24 12:10:24 +01:00
chucked: Boolean
2017-01-23 06:27:29 +01:00
): MediaStreamData? {
2017-01-23 14:31:14 +01:00
var mediaType = defaultType
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
}
2017-04-07 06:12:34 +02:00
geometry.x = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH).toIntOr(-1)
geometry.y = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT).toIntOr(-1)
duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION).toLongOr(-1)
framerate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CAPTURE_FRAMERATE).toDoubleOr(-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
}
if (videoLimit != null) {
if (geometry.x > 0 && geometry.y > 0 && videoLimit.checkGeometry(geometry.x, geometry.y)
&& framerate > 0 && videoLimit.checkFrameRate(framerate)
&& size > 0 && videoLimit.checkSize(size, chucked)) {
// Size valid, upload directly
DebugLog.d(LOGTAG, "Upload video directly")
return null
}
2017-01-23 14:31:14 +01:00
if (!videoLimit.checkMinDuration(duration, chucked)) {
throw UploadException(context.getString(R.string.message_video_too_short))
}
2017-01-24 12:10:24 +01:00
if (!videoLimit.checkMaxDuration(duration, chucked)) {
throw UploadException(context.getString(R.string.message_video_too_long))
}
2017-01-24 12:10:24 +01:00
}
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(
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-04-14 09:05:51 +02:00
inline fun createDraft(@Draft.Action action: String, config: Draft.() -> Unit): Draft {
2017-02-07 15:55:36 +01:00
val draft = Draft()
draft.action_type = action
draft.timestamp = System.currentTimeMillis()
config(draft)
2017-04-14 09:05:51 +02:00
return draft
}
fun saveDraft(context: Context, @Draft.Action action: String, config: Draft.() -> Unit): Long {
val draft = createDraft(action, config)
2017-02-07 15:55:36 +01:00
val resolver = context.contentResolver
2017-03-05 09:08:09 +01:00
val creator = ObjectCursor.valuesCreatorFrom(Draft::class.java)
val draftUri = resolver.insert(Drafts.CONTENT_URI, creator.create(draft)) ?: return -1
2017-04-07 06:12:34 +02:00
return draftUri.lastPathSegment.toLongOr(-1)
2017-02-07 15:55:36 +01:00
}
2017-04-14 09:05:51 +02:00
2017-02-07 15:55:36 +01:00
fun deleteDraft(context: Context, id: Long) {
2017-04-10 11:35:35 +02:00
val where = Expression.equals(Drafts._ID, id).sql
context.contentResolver.delete(Drafts.CONTENT_URI, where, null)
2017-02-07 15:55:36 +01:00
}
2016-07-16 09:27:46 +02:00
fun AccountExtras.ImageLimit.checkGeomentry(width: Int, height: Int): Boolean {
if (this.maxWidth <= 0 || this.maxHeight <= 0) return true
2017-04-08 11:00:13 +02:00
return (width <= this.maxWidth && height <= this.maxHeight) || (height <= this.maxWidth
&& width <= this.maxHeight)
}
2017-05-12 06:38:24 +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