2021-06-23 06:14:25 +02:00
|
|
|
package jp.juggler.subwaytooter.util
|
|
|
|
|
|
|
|
import android.content.ContentResolver
|
|
|
|
import android.content.Context
|
|
|
|
import android.graphics.Bitmap
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Handler
|
|
|
|
import android.os.SystemClock
|
|
|
|
import androidx.annotation.WorkerThread
|
|
|
|
import jp.juggler.subwaytooter.R
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiCallback
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiClient
|
|
|
|
import jp.juggler.subwaytooter.api.TootApiResult
|
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
|
|
|
import jp.juggler.subwaytooter.api.runApiTask
|
|
|
|
import jp.juggler.subwaytooter.table.SavedAccount
|
|
|
|
import jp.juggler.util.*
|
2022-01-05 23:29:05 +01:00
|
|
|
import jp.juggler.util.VideoInfo.Companion.videoInfo
|
2022-01-05 05:22:44 +01:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
import kotlinx.coroutines.channels.Channel
|
|
|
|
import kotlinx.coroutines.channels.ClosedReceiveChannelException
|
2021-06-23 06:14:25 +02:00
|
|
|
import kotlinx.coroutines.delay
|
2022-01-05 05:22:44 +01:00
|
|
|
import kotlinx.coroutines.isActive
|
|
|
|
import kotlinx.coroutines.withContext
|
2021-06-23 06:14:25 +02:00
|
|
|
import okhttp3.MediaType.Companion.toMediaType
|
|
|
|
import okhttp3.MultipartBody
|
|
|
|
import okhttp3.RequestBody
|
|
|
|
import okio.BufferedSink
|
|
|
|
import java.io.*
|
|
|
|
import java.util.*
|
2022-01-05 05:22:44 +01:00
|
|
|
import java.util.concurrent.CancellationException
|
|
|
|
import kotlin.coroutines.coroutineContext
|
2021-06-23 06:14:25 +02:00
|
|
|
|
|
|
|
class AttachmentRequest(
|
|
|
|
val account: SavedAccount,
|
|
|
|
val pa: PostAttachment,
|
|
|
|
val uri: Uri,
|
|
|
|
val mimeType: String,
|
|
|
|
val isReply: Boolean,
|
|
|
|
)
|
|
|
|
|
|
|
|
class AttachmentUploader(
|
|
|
|
contextArg: Context,
|
|
|
|
private val handler: Handler,
|
|
|
|
) {
|
|
|
|
companion object {
|
|
|
|
val log = LogCategory("AttachmentUploader")
|
|
|
|
|
|
|
|
internal const val MIME_TYPE_JPEG = "image/jpeg"
|
|
|
|
internal const val MIME_TYPE_PNG = "image/png"
|
|
|
|
|
|
|
|
val acceptableMimeTypes = HashSet<String>().apply {
|
|
|
|
//
|
|
|
|
add("image/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
|
|
|
|
add("video/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
|
|
|
|
add("audio/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
|
|
|
|
//
|
|
|
|
add("image/jpeg")
|
|
|
|
add("image/png")
|
|
|
|
add("image/gif")
|
|
|
|
add("video/webm")
|
|
|
|
add("video/mp4")
|
|
|
|
add("video/quicktime")
|
|
|
|
//
|
|
|
|
add("audio/webm")
|
|
|
|
add("audio/ogg")
|
|
|
|
add("audio/mpeg")
|
|
|
|
add("audio/mp3")
|
|
|
|
add("audio/wav")
|
|
|
|
add("audio/wave")
|
|
|
|
add("audio/x-wav")
|
|
|
|
add("audio/x-pn-wav")
|
|
|
|
add("audio/flac")
|
|
|
|
add("audio/x-flac")
|
|
|
|
|
|
|
|
// https://github.com/tootsuite/mastodon/pull/11342
|
|
|
|
add("audio/aac")
|
|
|
|
add("audio/m4a")
|
|
|
|
add("audio/3gpp")
|
|
|
|
}
|
|
|
|
|
|
|
|
val acceptableMimeTypesPixelfed = HashSet<String>().apply {
|
|
|
|
//
|
|
|
|
add("image/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
|
|
|
|
add("video/*") // Android標準のギャラリーが image/* を出してくることがあるらしい
|
|
|
|
//
|
|
|
|
add("image/jpeg")
|
|
|
|
add("image/png")
|
|
|
|
add("image/gif")
|
|
|
|
add("video/mp4")
|
|
|
|
add("video/m4v")
|
|
|
|
}
|
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
private val imageHeaderList = listOf(
|
2021-06-23 06:14:25 +02:00
|
|
|
Pair(
|
|
|
|
"image/jpeg",
|
|
|
|
intArrayOf(0xff, 0xd8, 0xff).toByteArray()
|
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"image/png",
|
|
|
|
intArrayOf(0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A).toByteArray()
|
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"image/gif",
|
2022-01-05 10:39:48 +01:00
|
|
|
"GIF".toByteArray(Charsets.UTF_8)
|
2021-06-23 06:14:25 +02:00
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"audio/wav",
|
2022-01-05 10:39:48 +01:00
|
|
|
"RIFF".toByteArray(Charsets.UTF_8),
|
2021-06-23 06:14:25 +02:00
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"audio/ogg",
|
2022-01-05 10:39:48 +01:00
|
|
|
"OggS".toByteArray(Charsets.UTF_8),
|
2021-06-23 06:14:25 +02:00
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"audio/flac",
|
2022-01-05 10:39:48 +01:00
|
|
|
"fLaC".toByteArray(Charsets.UTF_8),
|
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"image/bmp",
|
|
|
|
"BM".toByteArray(Charsets.UTF_8),
|
|
|
|
),
|
|
|
|
Pair(
|
|
|
|
"image/webp",
|
|
|
|
"RIFF****WEBP".toByteArray(Charsets.UTF_8),
|
|
|
|
),
|
|
|
|
).sortedByDescending { it.second.size }
|
2021-06-23 06:14:25 +02:00
|
|
|
|
|
|
|
private val sig3gp = arrayOf(
|
|
|
|
"3ge6",
|
|
|
|
"3ge7",
|
|
|
|
"3gg6",
|
|
|
|
"3gp1",
|
|
|
|
"3gp2",
|
|
|
|
"3gp3",
|
|
|
|
"3gp4",
|
|
|
|
"3gp5",
|
|
|
|
"3gp6",
|
|
|
|
"3gp7",
|
|
|
|
"3gr6",
|
|
|
|
"3gr7",
|
|
|
|
"3gs6",
|
|
|
|
"3gs7",
|
|
|
|
"kddi"
|
|
|
|
).map { it.toCharArray().toLowerByteArray() }
|
|
|
|
|
|
|
|
private val sigM4a = arrayOf(
|
|
|
|
"M4A ",
|
|
|
|
"M4B ",
|
|
|
|
"M4P "
|
|
|
|
).map { it.toCharArray().toLowerByteArray() }
|
|
|
|
|
|
|
|
private val sigFtyp = "ftyp".toCharArray().toLowerByteArray()
|
|
|
|
|
|
|
|
private fun matchSig(
|
|
|
|
data: ByteArray,
|
|
|
|
dataOffset: Int,
|
|
|
|
sig: ByteArray,
|
|
|
|
sigSize: Int = sig.size,
|
|
|
|
): Boolean {
|
|
|
|
for (i in 0 until sigSize) {
|
|
|
|
if (data[dataOffset + i] != sig[i]) return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
2022-01-05 10:39:48 +01:00
|
|
|
|
|
|
|
private const val wild = '?'.code.toByte()
|
|
|
|
|
|
|
|
private fun ByteArray.startWithWildcard(
|
|
|
|
key: ByteArray,
|
|
|
|
thisOffset: Int = 0,
|
|
|
|
keyOffset: Int = 0,
|
|
|
|
length: Int = key.size - keyOffset,
|
|
|
|
): Boolean {
|
|
|
|
if (thisOffset + length > this.size || keyOffset + length > key.size) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for (i in 0 until length) {
|
|
|
|
val cThis = this[i + thisOffset]
|
|
|
|
val cKey = key[i + keyOffset]
|
|
|
|
if (cKey != wild && cKey != cThis) return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
private val context = contextArg.applicationContext!!
|
2021-06-23 06:14:25 +02:00
|
|
|
private var lastAttachmentAdd = 0L
|
|
|
|
private var lastAttachmentComplete = 0L
|
2022-01-05 05:22:44 +01:00
|
|
|
private var channel: Channel<AttachmentRequest>? = null
|
|
|
|
|
|
|
|
private fun prepareChannel(): Channel<AttachmentRequest> {
|
|
|
|
// double check before/after lock
|
|
|
|
channel?.let { return it }
|
|
|
|
synchronized(this) {
|
|
|
|
channel?.let { return it }
|
|
|
|
return Channel<AttachmentRequest>(capacity = Channel.UNLIMITED)
|
|
|
|
.also {
|
|
|
|
channel = it
|
|
|
|
launchIO {
|
|
|
|
while (true) {
|
2022-01-05 10:39:48 +01:00
|
|
|
val request = try {
|
|
|
|
it.receive()
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
when (ex) {
|
|
|
|
is CancellationException, is ClosedReceiveChannelException -> break
|
|
|
|
else -> {
|
|
|
|
context.showToast(ex)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
val result = try {
|
|
|
|
if (request.pa.isCancelled) continue
|
|
|
|
withContext(request.pa.job + Dispatchers.IO) {
|
|
|
|
request.upload()
|
|
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
TootApiResult(ex.withCaption("upload failed."))
|
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
try {
|
2022-01-05 10:39:48 +01:00
|
|
|
request.pa.progress = ""
|
|
|
|
withContext(Dispatchers.Main) {
|
|
|
|
handleResult(request, result)
|
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
} catch (ex: Throwable) {
|
|
|
|
when (ex) {
|
|
|
|
is CancellationException, is ClosedReceiveChannelException -> break
|
2022-01-05 10:39:48 +01:00
|
|
|
else -> {
|
|
|
|
context.showToast(ex)
|
|
|
|
continue
|
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
|
|
|
fun onActivityDestroy() {
|
2022-01-05 05:22:44 +01:00
|
|
|
try {
|
|
|
|
synchronized(this) {
|
|
|
|
channel?.close()
|
|
|
|
channel = null
|
|
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.e(ex)
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
fun addRequest(request: AttachmentRequest) {
|
2022-01-05 10:39:48 +01:00
|
|
|
|
|
|
|
request.pa.progress = context.getString(R.string.attachment_handling_start)
|
|
|
|
|
2021-06-23 06:14:25 +02:00
|
|
|
// アップロード開始トースト(連発しない)
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
if (now - lastAttachmentAdd >= 5000L) {
|
|
|
|
context.showToast(false, R.string.attachment_uploading)
|
|
|
|
}
|
|
|
|
lastAttachmentAdd = now
|
|
|
|
|
|
|
|
// マストドンは添付メディアをID順に表示するため
|
|
|
|
// 画像が複数ある場合は一つずつ処理する必要がある
|
|
|
|
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う
|
2022-01-05 05:22:44 +01:00
|
|
|
launchIO { prepareChannel().send(request) }
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
@WorkerThread
|
|
|
|
private suspend fun AttachmentRequest.upload(): TootApiResult? {
|
|
|
|
try {
|
2022-01-05 10:39:48 +01:00
|
|
|
if (mimeType.isEmpty()) return TootApiResult("mime_type is empty.")
|
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val client = TootApiClient(context, callback = object : TootApiCallback {
|
|
|
|
override suspend fun isApiCancelled() = !coroutineContext.isActive
|
|
|
|
})
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
client.account = account
|
2022-01-05 10:39:48 +01:00
|
|
|
client.currentCallCallback = {}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val (ti, tiResult) = TootInstance.get(client)
|
|
|
|
ti ?: return tiResult
|
|
|
|
|
|
|
|
if (ti.instanceType == InstanceType.Pixelfed) {
|
|
|
|
if (isReply) {
|
|
|
|
return TootApiResult(context.getString(R.string.pixelfed_does_not_allow_reply_with_media))
|
|
|
|
}
|
|
|
|
if (!acceptableMimeTypesPixelfed.contains(mimeType)) {
|
|
|
|
return TootApiResult(
|
|
|
|
context.getString(
|
|
|
|
R.string.mime_type_not_acceptable,
|
|
|
|
mimeType
|
|
|
|
)
|
|
|
|
)
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
}
|
2022-01-05 10:39:48 +01:00
|
|
|
val mediaConfig = ti.configuration?.jsonObject("media_attachments")
|
|
|
|
val imageResizeConfig = mediaConfig?.int("image_matrix_limit")
|
|
|
|
?.takeIf { it > 0 }
|
|
|
|
?.let { ResizeConfig(ResizeType.SquarePixel, it) }
|
|
|
|
?: account.getResizeConfig()
|
|
|
|
|
|
|
|
// 入力データの変換など
|
|
|
|
val opener = createOpener(
|
2022-01-05 23:29:05 +01:00
|
|
|
account,
|
2022-01-05 10:39:48 +01:00
|
|
|
uri,
|
|
|
|
mimeType,
|
2022-01-05 11:11:45 +01:00
|
|
|
mediaConfig = mediaConfig,
|
2022-01-05 10:39:48 +01:00
|
|
|
imageResizeConfig = imageResizeConfig,
|
2022-01-05 11:11:45 +01:00
|
|
|
postAttachment = pa,
|
2022-01-05 10:39:48 +01:00
|
|
|
)
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val mediaSizeMax = when {
|
|
|
|
mimeType.startsWith("video") || mimeType.startsWith("audio") ->
|
2022-01-05 10:39:48 +01:00
|
|
|
mediaConfig?.int("video_size_limit")
|
|
|
|
?.takeIf { it > 0 }
|
|
|
|
?: account.getMovieMaxBytes(ti)
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
else -> mediaConfig?.int("image_size_limit")
|
|
|
|
?.takeIf { it > 0 }
|
|
|
|
?: account.getImageMaxBytes(ti)
|
2022-01-05 05:22:44 +01:00
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
if (opener.contentLength > mediaSizeMax) {
|
2022-01-05 05:22:44 +01:00
|
|
|
return TootApiResult(
|
|
|
|
context.getString(R.string.file_size_too_big, mediaSizeMax / 1000000)
|
|
|
|
)
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
fun fixDocumentName(s: String): String {
|
|
|
|
val sLength = s.length
|
|
|
|
val m = """([^\x20-\x7f])""".asciiPattern().matcher(s)
|
|
|
|
m.reset()
|
|
|
|
val sb = StringBuilder(sLength)
|
|
|
|
var lastEnd = 0
|
|
|
|
while (m.find()) {
|
|
|
|
sb.append(s.substring(lastEnd, m.start()))
|
|
|
|
val escaped = m.groupEx(1)!!.encodeUTF8().encodeHex()
|
|
|
|
sb.append(escaped)
|
|
|
|
lastEnd = m.end()
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
if (lastEnd < sLength) sb.append(s.substring(lastEnd, sLength))
|
|
|
|
return sb.toString()
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val fileName = fixDocumentName(getDocumentName(context.contentResolver, uri))
|
2022-01-05 10:39:48 +01:00
|
|
|
pa.progress = context.getString(R.string.attachment_handling_uploading, 0)
|
2022-01-05 23:29:05 +01:00
|
|
|
fun writeProgress(percent: Int) {
|
|
|
|
if (percent < 100) {
|
|
|
|
pa.progress =
|
|
|
|
context.getString(R.string.attachment_handling_uploading, percent)
|
|
|
|
} else {
|
|
|
|
pa.progress = context.getString(R.string.attachment_handling_waiting)
|
2022-01-05 10:39:48 +01:00
|
|
|
}
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
return if (account.isMisskey) {
|
|
|
|
val multipartBuilder = MultipartBody.Builder()
|
|
|
|
.setType(MultipartBody.FORM)
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val apiKey = account.token_info?.string(TootApiClient.KEY_API_KEY_MISSKEY)
|
|
|
|
if (apiKey?.isNotEmpty() == true) {
|
|
|
|
multipartBuilder.addFormDataPart("i", apiKey)
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
multipartBuilder.addFormDataPart(
|
|
|
|
"file",
|
|
|
|
fileName,
|
2022-01-05 23:29:05 +01:00
|
|
|
opener.toRequestBody { writeProgress(it) },
|
2022-01-05 05:22:44 +01:00
|
|
|
)
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val result = client.request(
|
|
|
|
"/api/drive/files/create",
|
|
|
|
multipartBuilder.build().toPost()
|
|
|
|
)
|
|
|
|
opener.deleteTempFile()
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val jsonObject = result?.jsonObject
|
|
|
|
if (jsonObject != null) {
|
|
|
|
val a = parseItem(::TootAttachment, ServiceType.MISSKEY, jsonObject)
|
|
|
|
if (a == null) {
|
|
|
|
result.error = "TootAttachment.parse failed"
|
|
|
|
} else {
|
|
|
|
pa.attachment = a
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
}
|
|
|
|
result
|
|
|
|
} else {
|
|
|
|
suspend fun postMedia(path: String) = client.request(
|
|
|
|
path,
|
|
|
|
MultipartBody.Builder()
|
|
|
|
.setType(MultipartBody.FORM)
|
|
|
|
.addFormDataPart(
|
|
|
|
"file",
|
|
|
|
fileName,
|
2022-01-05 23:29:05 +01:00
|
|
|
opener.toRequestBody { writeProgress(it) },
|
2022-01-05 05:22:44 +01:00
|
|
|
)
|
|
|
|
.build().toPost()
|
|
|
|
)
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
suspend fun postV1() = postMedia("/api/v1/media")
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
suspend fun postV2(): TootApiResult? {
|
|
|
|
// 3.1.3未満は v1 APIを使う
|
|
|
|
if (!ti.versionGE(TootInstance.VERSION_3_1_3)) {
|
|
|
|
return postV1()
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
// v2 APIを試す
|
|
|
|
val result = postMedia("/api/v2/media")
|
|
|
|
val code = result?.response?.code // complete,or 4xx error
|
|
|
|
when {
|
|
|
|
// 404ならv1 APIにフォールバック
|
|
|
|
code == 404 -> return postV1()
|
|
|
|
// 202 accepted 以外はポーリングしない
|
|
|
|
code != 202 -> return result
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
// ポーリングして処理完了を待つ
|
2022-01-05 23:29:05 +01:00
|
|
|
pa.progress = context.getString(R.string.attachment_handling_waiting_async)
|
|
|
|
val id = parseItem(::TootAttachment, ServiceType.MASTODON, result?.jsonObject)
|
|
|
|
?.id
|
|
|
|
?: return TootApiResult("/api/v2/media did not return the media ID.")
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
var lastResponse = SystemClock.elapsedRealtime()
|
|
|
|
loop@ while (true) {
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
delay(1000L)
|
2022-01-05 23:29:05 +01:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val r2 = client.request("/api/v1/media/$id")
|
|
|
|
?: return null // cancelled
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
when (r2.response?.code) {
|
|
|
|
// complete,or 4xx error
|
|
|
|
200, in 400 until 500 -> return r2
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
// continue to wait
|
|
|
|
206 -> lastResponse = now
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
// temporary errors, check timeout without 206 response.
|
2022-01-05 05:22:44 +01:00
|
|
|
else -> if (now - lastResponse >= 120000L) {
|
|
|
|
return TootApiResult("timeout.")
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val result = postV2()
|
|
|
|
opener.deleteTempFile()
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
val jsonObject = result?.jsonObject
|
|
|
|
if (jsonObject != null) {
|
|
|
|
when (val a =
|
|
|
|
parseItem(::TootAttachment, ServiceType.MASTODON, jsonObject)) {
|
|
|
|
null -> result.error = "TootAttachment.parse failed"
|
|
|
|
else -> pa.attachment = a
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
result
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
} catch (ex: Throwable) {
|
|
|
|
return TootApiResult(ex.withCaption("read failed."))
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
private fun handleResult(request: AttachmentRequest, result: TootApiResult?) {
|
|
|
|
val pa = request.pa
|
|
|
|
pa.status = when (pa.attachment) {
|
|
|
|
null -> {
|
|
|
|
if (result != null) {
|
2022-01-05 10:39:48 +01:00
|
|
|
when {
|
|
|
|
// キャンセルはトーストを出さない
|
|
|
|
result.error?.contains("cancel", ignoreCase = true) == true -> Unit
|
|
|
|
else -> context.showToast(
|
|
|
|
true,
|
|
|
|
"${result.error} ${result.response?.request?.method} ${result.response?.request?.url}"
|
|
|
|
)
|
|
|
|
}
|
2022-01-05 05:22:44 +01:00
|
|
|
}
|
|
|
|
PostAttachment.Status.Error
|
|
|
|
}
|
|
|
|
else -> {
|
|
|
|
val now = System.currentTimeMillis()
|
|
|
|
if (now - lastAttachmentComplete >= 5000L) {
|
|
|
|
context.showToast(false, R.string.attachment_uploaded)
|
|
|
|
}
|
|
|
|
lastAttachmentComplete = now
|
|
|
|
|
|
|
|
PostAttachment.Status.Ok
|
|
|
|
}
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 05:22:44 +01:00
|
|
|
// 投稿中に画面回転があった場合、新しい画面のコールバックを呼び出す必要がある
|
|
|
|
pa.callback?.onPostAttachmentComplete(pa)
|
|
|
|
}
|
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
// contentLengthの測定などで複数回オープンする必要がある
|
|
|
|
private abstract class InputStreamOpener {
|
|
|
|
abstract val mimeType: String
|
2021-06-23 06:14:25 +02:00
|
|
|
|
|
|
|
@Throws(IOException::class)
|
2022-01-05 23:29:05 +01:00
|
|
|
abstract fun open(): InputStream
|
|
|
|
|
|
|
|
abstract fun deleteTempFile()
|
|
|
|
|
|
|
|
val contentLength by lazy { getStreamSize(true, open()) }
|
|
|
|
|
|
|
|
// okhttpのRequestBodyにする
|
|
|
|
fun toRequestBody(onWrote: (percent: Int) -> Unit = {}) =
|
|
|
|
object : RequestBody() {
|
|
|
|
override fun contentType() = mimeType.toMediaType()
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
override fun contentLength(): Long = contentLength
|
2021-06-23 06:14:25 +02:00
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
@Throws(IOException::class)
|
|
|
|
override fun writeTo(sink: BufferedSink) {
|
|
|
|
val length = contentLength.toFloat()
|
|
|
|
open().use { inStream ->
|
|
|
|
val tmp = ByteArray(4096)
|
|
|
|
var nWrite = 0L
|
|
|
|
while (true) {
|
|
|
|
val delta = inStream.read(tmp, 0, tmp.size)
|
|
|
|
if (delta <= 0) break
|
|
|
|
sink.write(tmp, 0, delta)
|
|
|
|
nWrite += delta
|
|
|
|
val percent = (100f * nWrite.toFloat() / length).toInt()
|
|
|
|
onWrote(percent)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
private fun contentUriOpener(contentResolver: ContentResolver, uri: Uri, mimeType: String) =
|
|
|
|
object : InputStreamOpener() {
|
|
|
|
override val mimeType = mimeType
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
override fun open(): InputStream {
|
|
|
|
return contentResolver.openInputStream(uri)
|
|
|
|
?: error("openInputStream returns null")
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun deleteTempFile() = Unit
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun tempFileOpener(mimeType: String, file: File) =
|
|
|
|
object : InputStreamOpener() {
|
|
|
|
override val mimeType = mimeType
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
override fun open() = FileInputStream(file)
|
|
|
|
override fun deleteTempFile() {
|
|
|
|
file.delete()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
private suspend fun createOpener(
|
2022-01-05 23:29:05 +01:00
|
|
|
account: SavedAccount,
|
2021-06-23 06:14:25 +02:00
|
|
|
uri: Uri,
|
|
|
|
mimeType: String,
|
2022-01-05 11:11:45 +01:00
|
|
|
mediaConfig: JsonObject? = null,
|
2022-01-05 10:39:48 +01:00
|
|
|
imageResizeConfig: ResizeConfig,
|
|
|
|
postAttachment: PostAttachment? = null,
|
2021-06-23 06:14:25 +02:00
|
|
|
): InputStreamOpener {
|
2022-01-05 23:35:34 +01:00
|
|
|
when {
|
2022-01-05 23:29:05 +01:00
|
|
|
// 静止画(失敗したらオリジナルデータにフォールバックする)
|
2022-01-05 23:35:34 +01:00
|
|
|
mimeType == MIME_TYPE_JPEG || mimeType == MIME_TYPE_PNG -> try {
|
2022-01-05 10:39:48 +01:00
|
|
|
return createResizedImageOpener(
|
2021-06-23 06:14:25 +02:00
|
|
|
uri,
|
2022-01-05 10:39:48 +01:00
|
|
|
mimeType,
|
|
|
|
imageResizeConfig,
|
|
|
|
postAttachment,
|
2021-06-23 06:14:25 +02:00
|
|
|
)
|
|
|
|
} catch (ex: Throwable) {
|
2022-01-05 10:39:48 +01:00
|
|
|
log.w(ex, "createResizedImageOpener failed. fall back to original image.")
|
|
|
|
}
|
2022-01-05 23:35:34 +01:00
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
// 静止画(変換必須)
|
|
|
|
// 例外を投げるかもしれない
|
2022-01-05 23:35:34 +01:00
|
|
|
mimeType.startsWith("image/") ->
|
|
|
|
return createResizedImageOpener(
|
|
|
|
uri,
|
|
|
|
mimeType,
|
|
|
|
imageResizeConfig,
|
|
|
|
postAttachment,
|
|
|
|
forcePng = true
|
|
|
|
)
|
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
// 動画のトランスコード(失敗したらオリジナルデータにフォールバックする)
|
2022-01-05 23:35:34 +01:00
|
|
|
mimeType.startsWith("video/") -> try {
|
2022-01-05 10:39:48 +01:00
|
|
|
return createResizedMovieOpener(
|
2022-01-05 23:29:05 +01:00
|
|
|
account,
|
2022-01-05 10:39:48 +01:00
|
|
|
uri,
|
|
|
|
mimeType,
|
2022-01-05 11:11:45 +01:00
|
|
|
mediaConfig = mediaConfig,
|
2022-01-05 23:29:05 +01:00
|
|
|
postAttachment = postAttachment,
|
2022-01-05 10:39:48 +01:00
|
|
|
)
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.w(ex, "createResizedMovieOpener failed. fall back to original movie.")
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
return contentUriOpener(context.contentResolver, uri, mimeType)
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
private fun createResizedImageOpener(
|
|
|
|
uri: Uri,
|
|
|
|
mimeType: String,
|
|
|
|
imageResizeConfig: ResizeConfig,
|
|
|
|
postAttachment: PostAttachment? = null,
|
|
|
|
forcePng: Boolean = false,
|
|
|
|
): InputStreamOpener {
|
|
|
|
val cacheDir = context.externalCacheDir
|
|
|
|
?.apply { mkdirs() }
|
|
|
|
?: error("getExternalCacheDir returns null.")
|
|
|
|
|
|
|
|
val outputMimeType = if (forcePng || mimeType == MIME_TYPE_PNG) {
|
|
|
|
MIME_TYPE_PNG
|
|
|
|
} else {
|
|
|
|
MIME_TYPE_JPEG
|
|
|
|
}
|
|
|
|
|
|
|
|
val tempFile = File(cacheDir, "tmp." + Thread.currentThread().id)
|
|
|
|
val bitmap = createResizedBitmap(
|
|
|
|
context,
|
|
|
|
uri,
|
|
|
|
imageResizeConfig,
|
|
|
|
skipIfNoNeedToResizeAndRotate = !forcePng
|
|
|
|
) ?: error("createResizedBitmap returns null.")
|
|
|
|
postAttachment?.progress = context.getString(R.string.attachment_handling_compress)
|
|
|
|
try {
|
|
|
|
FileOutputStream(tempFile).use { os ->
|
|
|
|
if (outputMimeType == MIME_TYPE_PNG) {
|
|
|
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, os)
|
|
|
|
} else {
|
|
|
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, os)
|
|
|
|
}
|
|
|
|
}
|
2022-01-05 23:29:05 +01:00
|
|
|
return tempFileOpener(outputMimeType, tempFile)
|
2022-01-05 10:39:48 +01:00
|
|
|
} finally {
|
|
|
|
bitmap.recycle()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun createResizedMovieOpener(
|
2022-01-05 23:29:05 +01:00
|
|
|
account: SavedAccount,
|
2022-01-05 10:39:48 +01:00
|
|
|
uri: Uri,
|
|
|
|
mimeType: String,
|
2022-01-05 11:11:45 +01:00
|
|
|
mediaConfig: JsonObject?,
|
|
|
|
postAttachment: PostAttachment?,
|
2022-01-05 10:39:48 +01:00
|
|
|
): InputStreamOpener {
|
|
|
|
val cacheDir = context.externalCacheDir
|
|
|
|
?.apply { mkdirs() }
|
|
|
|
?: error("getExternalCacheDir returns null.")
|
|
|
|
|
|
|
|
val tempFile = File(cacheDir, "movie." + Thread.currentThread().id + ".tmp")
|
|
|
|
val outFile = File(cacheDir, "movie." + Thread.currentThread().id + ".mp4")
|
2022-01-05 11:11:45 +01:00
|
|
|
var resultFile: File? = null
|
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
// 入力ファイルをコピーする
|
|
|
|
(context.contentResolver.openInputStream(uri)
|
|
|
|
?: error("openInputStream returns null.")).use { inStream ->
|
|
|
|
FileOutputStream(tempFile).use { inStream.copyTo(it) }
|
|
|
|
}
|
2022-01-05 11:11:45 +01:00
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
try {
|
2022-01-05 23:29:05 +01:00
|
|
|
// 動画のメタデータを調べる
|
|
|
|
val info = tempFile.videoInfo
|
|
|
|
|
|
|
|
// サーバに指定されたファイルサイズ上限と入力動画の時間長があれば、ビットレート上限を制限する
|
|
|
|
val duration = info.duration?.takeIf { it >= 0.1f }
|
|
|
|
val limitFileSize = mediaConfig?.float("video_size_limit")?.takeIf { it >= 1f }
|
|
|
|
val limitBitrate = when {
|
|
|
|
duration != null && limitFileSize != null ->
|
|
|
|
(limitFileSize / duration).toLong()
|
|
|
|
else -> null
|
|
|
|
}
|
|
|
|
|
|
|
|
// アカウント別の動画トランスコード設定
|
|
|
|
// ビットレート、フレームレート、平方ピクセル数をサーバ指定により制限する
|
|
|
|
val movieResizeConfig = account.getMovieResizeConfig()
|
|
|
|
.restrict(
|
|
|
|
limitBitrate = limitBitrate,
|
|
|
|
limitFrameRate = mediaConfig?.int("video_frame_rate_limit")
|
|
|
|
?.takeIf { it >= 1f },
|
|
|
|
limitSquarePixels = mediaConfig?.int("video_matrix_limit")
|
|
|
|
?.takeIf { it > 1 },
|
|
|
|
)
|
2022-01-05 11:11:45 +01:00
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
val result = transcodeVideo(
|
2022-01-05 23:29:05 +01:00
|
|
|
info,
|
2022-01-05 10:39:48 +01:00
|
|
|
tempFile,
|
|
|
|
outFile,
|
|
|
|
movieResizeConfig,
|
|
|
|
) {
|
|
|
|
val percent = (it * 100f).toInt()
|
|
|
|
postAttachment?.progress =
|
|
|
|
context.getString(R.string.attachment_handling_compress_ratio, percent)
|
|
|
|
}
|
|
|
|
resultFile = result
|
2022-01-05 23:29:05 +01:00
|
|
|
return tempFileOpener(
|
2022-01-05 10:39:48 +01:00
|
|
|
when (result) {
|
|
|
|
tempFile -> mimeType
|
|
|
|
else -> "video/mp4"
|
|
|
|
},
|
|
|
|
result
|
|
|
|
)
|
|
|
|
} finally {
|
|
|
|
if (outFile != resultFile) outFile.delete()
|
|
|
|
if (tempFile != resultFile) tempFile.delete()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-23 06:14:25 +02:00
|
|
|
fun getMimeType(uri: Uri, mimeTypeArg: String?): String? {
|
|
|
|
// image/j()pg だの image/j(e)pg だの、mime type を誤記するアプリがあまりに多い
|
|
|
|
// クレームで消耗するのを減らすためにファイルヘッダを確認する
|
|
|
|
if (mimeTypeArg == null || mimeTypeArg.startsWith("image/")) {
|
|
|
|
val sv = findMimeTypeByFileHeader(context.contentResolver, uri)
|
|
|
|
if (sv != null) return sv
|
|
|
|
}
|
|
|
|
|
|
|
|
// 既に引数で与えられてる
|
|
|
|
if (mimeTypeArg?.isNotEmpty() == true) {
|
|
|
|
return mimeTypeArg
|
|
|
|
}
|
|
|
|
|
|
|
|
// ContentResolverに尋ねる
|
|
|
|
var sv = context.contentResolver.getType(uri)
|
|
|
|
if (sv?.isNotEmpty() == true) return sv
|
|
|
|
|
|
|
|
// gboardのステッカーではUriのクエリパラメータにmimeType引数がある
|
|
|
|
sv = uri.getQueryParameter("mimeType")
|
|
|
|
if (sv?.isNotEmpty() == true) return sv
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun findMimeTypeByFileHeader(
|
|
|
|
contentResolver: ContentResolver,
|
|
|
|
uri: Uri,
|
|
|
|
): String? {
|
|
|
|
try {
|
|
|
|
contentResolver.openInputStream(uri)?.use { inStream ->
|
|
|
|
val data = ByteArray(65536)
|
|
|
|
val nRead = inStream.read(data, 0, data.size)
|
|
|
|
for (pair in imageHeaderList) {
|
|
|
|
val type = pair.first
|
|
|
|
val header = pair.second
|
2022-01-05 10:39:48 +01:00
|
|
|
if (nRead >= header.size && data.startWithWildcard(header)) return type
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// scan frame header
|
|
|
|
for (i in 0 until nRead - 8) {
|
|
|
|
|
|
|
|
if (!matchSig(data, i, sigFtyp)) continue
|
|
|
|
|
|
|
|
// 3gpp check
|
|
|
|
for (s in sig3gp) {
|
|
|
|
if (matchSig(data, i + 4, s)) return "audio/3gpp"
|
|
|
|
}
|
|
|
|
|
|
|
|
// m4a check
|
|
|
|
for (s in sigM4a) {
|
|
|
|
if (matchSig(data, i + 4, s)) return "audio/m4a"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// scan frame header
|
|
|
|
loop@ for (i in 0 until nRead - 2) {
|
|
|
|
|
|
|
|
// mpeg frame header
|
|
|
|
val b0 = data[i].toInt() and 255
|
|
|
|
if (b0 != 255) continue
|
|
|
|
val b1 = data[i + 1].toInt() and 255
|
|
|
|
if ((b1 and 0b11100000) != 0b11100000) continue
|
|
|
|
|
|
|
|
val mpegVersionId = ((b1 shr 3) and 3)
|
|
|
|
// 00 mpeg 2.5
|
|
|
|
// 01 not used
|
|
|
|
// 10 (mp3) mpeg 2 / (AAC) mpeg-4
|
|
|
|
// 11 (mp3) mpeg 1 / (AAC) mpeg-2
|
|
|
|
|
|
|
|
@Suppress("MoveVariableDeclarationIntoWhen")
|
|
|
|
val mpegLayerId = ((b1 shr 1) and 3)
|
|
|
|
// 00 (mp3)not used / (AAC) always 0
|
|
|
|
// 01 (mp3)layer III
|
|
|
|
// 10 (mp3)layer II
|
|
|
|
// 11 (mp3)layer I
|
|
|
|
|
|
|
|
when (mpegLayerId) {
|
|
|
|
0 -> when (mpegVersionId) {
|
|
|
|
2, 3 -> return "audio/aac"
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
1 -> when (mpegVersionId) {
|
|
|
|
0, 2, 3 -> return "audio/mp3"
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.e(ex, "findMimeTypeByFileHeader failed.")
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
2022-01-05 23:29:05 +01:00
|
|
|
// 添付データのカスタムサムネイル
|
2021-06-23 06:14:25 +02:00
|
|
|
suspend fun uploadCustomThumbnail(
|
|
|
|
account: SavedAccount,
|
|
|
|
src: GetContentResultEntry,
|
|
|
|
pa: PostAttachment,
|
|
|
|
): TootApiResult? = try {
|
|
|
|
context.runApiTask(account) { client ->
|
|
|
|
val mimeType = getMimeType(src.uri, src.mimeType)
|
|
|
|
if (mimeType?.isEmpty() != false) {
|
|
|
|
return@runApiTask TootApiResult(context.getString(R.string.mime_type_missing))
|
|
|
|
}
|
|
|
|
|
|
|
|
val (ti, ri) = TootInstance.get(client)
|
|
|
|
ti ?: return@runApiTask ri
|
|
|
|
|
2022-01-05 23:29:05 +01:00
|
|
|
val opener = createOpener(
|
|
|
|
account,
|
|
|
|
src.uri,
|
|
|
|
mimeType,
|
|
|
|
imageResizeConfig = ResizeConfig(ResizeType.SquarePixel, 400)
|
|
|
|
)
|
2021-06-23 06:14:25 +02:00
|
|
|
|
|
|
|
val mediaSizeMax = 1000000
|
2022-01-05 23:29:05 +01:00
|
|
|
if (opener.contentLength > mediaSizeMax) {
|
2021-06-23 06:14:25 +02:00
|
|
|
return@runApiTask TootApiResult(
|
|
|
|
getString(R.string.file_size_too_big, mediaSizeMax / 1000000)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun fixDocumentName(s: String): String {
|
|
|
|
val sLength = s.length
|
|
|
|
val m = """([^\x20-\x7f])""".asciiPattern().matcher(s)
|
|
|
|
m.reset()
|
|
|
|
val sb = StringBuilder(sLength)
|
|
|
|
var lastEnd = 0
|
|
|
|
while (m.find()) {
|
|
|
|
sb.append(s.substring(lastEnd, m.start()))
|
|
|
|
val escaped = m.groupEx(1)!!.encodeUTF8().encodeHex()
|
|
|
|
sb.append(escaped)
|
|
|
|
lastEnd = m.end()
|
|
|
|
}
|
|
|
|
if (lastEnd < sLength) sb.append(s.substring(lastEnd, sLength))
|
|
|
|
return sb.toString()
|
|
|
|
}
|
|
|
|
|
|
|
|
val fileName = fixDocumentName(getDocumentName(context.contentResolver, src.uri))
|
|
|
|
|
|
|
|
if (account.isMisskey) {
|
|
|
|
opener.deleteTempFile()
|
|
|
|
TootApiResult("custom thumbnail is not supported on misskey account.")
|
|
|
|
} else {
|
|
|
|
val result = client.request(
|
|
|
|
"/api/v1/media/${pa.attachment?.id}",
|
|
|
|
MultipartBody.Builder()
|
|
|
|
.setType(MultipartBody.FORM)
|
|
|
|
.addFormDataPart(
|
|
|
|
"thumbnail",
|
|
|
|
fileName,
|
2022-01-05 23:29:05 +01:00
|
|
|
opener.toRequestBody(),
|
2021-06-23 06:14:25 +02:00
|
|
|
)
|
|
|
|
.build().toPut()
|
|
|
|
)
|
|
|
|
opener.deleteTempFile()
|
|
|
|
|
|
|
|
val jsonObject = result?.jsonObject
|
|
|
|
if (jsonObject != null) {
|
|
|
|
val a = parseItem(::TootAttachment, ServiceType.MASTODON, jsonObject)
|
|
|
|
if (a == null) {
|
|
|
|
result.error = "TootAttachment.parse failed"
|
|
|
|
} else {
|
|
|
|
pa.attachment = a
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
TootApiResult(ex.withCaption("uploadCustomThumbnail failed."))
|
|
|
|
}
|
|
|
|
|
|
|
|
suspend fun setAttachmentDescription(
|
|
|
|
account: SavedAccount,
|
|
|
|
attachmentId: EntityId,
|
|
|
|
description: String,
|
|
|
|
): Pair<TootApiResult?, TootAttachment?> {
|
|
|
|
var resultAttachment: TootAttachment? = null
|
|
|
|
val result = try {
|
|
|
|
context.runApiTask(account) { client ->
|
|
|
|
client.request(
|
|
|
|
"/api/v1/media/$attachmentId",
|
|
|
|
jsonObject {
|
|
|
|
put("description", description)
|
|
|
|
}
|
|
|
|
.toPutRequestBuilder()
|
|
|
|
)?.also { result ->
|
2022-01-05 05:22:44 +01:00
|
|
|
resultAttachment =
|
|
|
|
parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
|
2021-06-23 06:14:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
log.trace(ex, "setAttachmentDescription failed.")
|
|
|
|
TootApiResult(ex.withCaption("setAttachmentDescription failed."))
|
|
|
|
}
|
|
|
|
return Pair(result, resultAttachment)
|
|
|
|
}
|
|
|
|
|
2022-01-05 10:39:48 +01:00
|
|
|
fun isAcceptableMimeType(
|
|
|
|
instance: TootInstance?,
|
|
|
|
mimeType: String,
|
|
|
|
isReply: Boolean,
|
|
|
|
): Boolean {
|
2021-06-23 06:14:25 +02:00
|
|
|
if (instance?.instanceType == InstanceType.Pixelfed) {
|
|
|
|
if (isReply) {
|
|
|
|
context.showToast(true, R.string.pixelfed_does_not_allow_reply_with_media)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if (!acceptableMimeTypesPixelfed.contains(mimeType)) {
|
|
|
|
context.showToast(true, R.string.mime_type_not_acceptable, mimeType)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (!acceptableMimeTypes.contains(mimeType)) {
|
|
|
|
context.showToast(true, R.string.mime_type_not_acceptable, mimeType)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|