TootApiClientのisApiCancelleをval から suspend funに変更

This commit is contained in:
tateisu 2022-01-05 13:22:44 +09:00
parent c7ec0fa3cd
commit b0ddddfe49
17 changed files with 325 additions and 317 deletions

View File

@ -541,7 +541,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
request
}) return Pair(result, null)
if (client.isApiCancelled) return Pair(null, null)
if (client.isApiCancelled()) return Pair(null, null)
val response = result.response!!
if (!response.isSuccessful) {
@ -557,7 +557,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
}
client.publishApiProgressRatio(bytesRead.toInt(), bytesTotal.toInt())
}
if (client.isApiCancelled) return Pair(null, null)
if (client.isApiCancelled()) return Pair(null, null)
return Pair(result, ba)
} catch (ignored: Throwable) {
result.parseErrorResponse("?")

View File

@ -344,7 +344,7 @@ suspend fun Context.accountListWithFilter(
.mapNotNull { it.await() }
.let { accountListReorder(it, pickupHost) }
}
if (client.isApiCancelled) null else TootApiResult()
if (client.isApiCancelled()) null else TootApiResult()
}
return resultList
}
@ -352,7 +352,7 @@ suspend fun Context.accountListWithFilter(
suspend fun ActMain.accountListCanQuote(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a ->
when {
client.isApiCancelled -> false
client.isApiCancelled() -> false
a.isPseudo -> false
a.isMisskey -> true
else -> {
@ -368,7 +368,7 @@ suspend fun ActMain.accountListCanQuote(pickupHost: Host? = null) =
suspend fun ActMain.accountListCanReaction(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a ->
when {
client.isApiCancelled -> false
client.isApiCancelled() -> false
a.isPseudo -> false
a.isMisskey -> true
else -> {
@ -384,7 +384,7 @@ suspend fun ActMain.accountListCanReaction(pickupHost: Host? = null) =
suspend fun ActMain.accountListCanSeeMyReactions(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a ->
when {
client.isApiCancelled -> false
client.isApiCancelled() -> false
a.isPseudo -> false
else -> {
val (ti, ri) = TootInstance.getEx(client.copy(), account = a)

View File

@ -142,7 +142,8 @@ fun ActPost.restoreDraft(draft: JsonObject) {
fun isTaskCancelled() = !this.coroutineContext.isActive
var content = draft.string(DRAFT_CONTENT) ?: ""
val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST)?.objectList()?.toMutableList()
val tmpAttachmentList =
draft.jsonArray(DRAFT_ATTACHMENT_LIST)?.objectList()?.toMutableList()
val accountDbId = draft.long(DRAFT_ACCOUNT_DB_ID) ?: -1L
val account = SavedAccount.loadAccount(this@restoreDraft, accountDbId)
@ -175,8 +176,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
// アカウントがあるなら基本的にはすべての情報を復元できるはずだが、いくつか確認が必要だ
val apiClient = TootApiClient(this@restoreDraft, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = isTaskCancelled()
override suspend fun isApiCancelled() = isTaskCancelled()
override suspend fun publishApiProgress(s: String) {
progress.setMessageEx(s)
@ -236,7 +236,8 @@ fun ActPost.restoreDraft(draft: JsonObject) {
val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST)
val replyId = EntityId.from(draft, DRAFT_REPLY_ID)
val draftVisibility = TootVisibility.parseSavedVisibility(draft.string(DRAFT_VISIBILITY))
val draftVisibility =
TootVisibility.parseSavedVisibility(draft.string(DRAFT_VISIBILITY))
val evEmoji = DecodeOptions(this@restoreDraft, decodeEmoji = true)
.decodeEmoji(content)
@ -381,7 +382,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
}
srcEnquete.pollType == TootPollsType.FriendsNico &&
srcEnquete.type != TootPolls.TYPE_ENQUETE -> {
srcEnquete.type != TootPolls.TYPE_ENQUETE -> {
// フレニコAPIのアンケート結果は再編集の対象外
}

View File

@ -120,8 +120,7 @@ private class TootTaskRunner(
//////////////////////////////////////////////////////
// implements TootApiClient.Callback
override val isApiCancelled: Boolean
get() = task?.isActive == false
override suspend fun isApiCancelled() = task?.isActive == false
override suspend fun publishApiProgress(s: String) {
synchronized(this) {

View File

@ -1,7 +1,7 @@
package jp.juggler.subwaytooter.api
interface TootApiCallback {
val isApiCancelled: Boolean
suspend fun isApiCancelled(): Boolean
suspend fun publishApiProgress(s: String) {}
suspend fun publishApiProgressRatio(value: Int, max: Int) {}
}

View File

@ -1,7 +1,8 @@
package jp.juggler.subwaytooter.api
import android.content.Context
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.pref
@ -169,8 +170,7 @@ class TootApiClient(
}
@Suppress("unused")
internal val isApiCancelled: Boolean
get() = callback.isApiCancelled
internal suspend fun isApiCancelled() = callback.isApiCancelled()
suspend fun publishApiProgress(s: String) {
callback.publishApiProgress(s)
@ -271,7 +271,7 @@ class TootApiClient(
): String? {
val response = result.response ?: return null
try {
if (isApiCancelled) return null
if (isApiCancelled()) return null
val request = response.request
publishApiProgress(
@ -283,7 +283,7 @@ class TootApiClient(
)
val bodyString = response.body?.string()
if (isApiCancelled) return null
if (isApiCancelled()) return null
// Misskey の /api/notes/favorites/create は 204(no content)を返す。ボディはカラになる。
if (bodyString?.isEmpty() != false && response.code in 200 until 300) {
@ -305,7 +305,7 @@ class TootApiClient(
result.bodyString = bodyString
bodyString
}
}finally{
} finally {
response.body?.closeQuietly()
}
}
@ -318,7 +318,7 @@ class TootApiClient(
jsonErrorParser: (json: JsonObject) -> String? = DEFAULT_JSON_ERROR_PARSER,
): ByteArray? {
if (isApiCancelled) return null
if (isApiCancelled()) return null
val response = result.response!!
@ -332,7 +332,7 @@ class TootApiClient(
)
val bodyBytes = response.body?.bytes()
if (isApiCancelled) return null
if (isApiCancelled()) return null
if (!response.isSuccessful || bodyBytes?.isEmpty() != false) {
result.parseErrorResponse(
@ -357,7 +357,7 @@ class TootApiClient(
): TootApiResult? {
try {
readBodyBytes(result, progressPath, jsonErrorParser)
?: return if (isApiCancelled) null else result
?: return if (isApiCancelled()) null else result
} catch (ex: Throwable) {
log.trace(ex)
result.parseErrorResponse(result.bodyString ?: NO_INFORMATION)
@ -372,7 +372,7 @@ class TootApiClient(
): TootApiResult? {
try {
val bodyString = readBodyString(result, progressPath, jsonErrorParser)
?: return if (isApiCancelled) null else result
?: return if (isApiCancelled()) null else result
result.data = bodyString
} catch (ex: Throwable) {
@ -393,7 +393,7 @@ class TootApiClient(
try {
var bodyString = readBodyString(result, progressPath, jsonErrorParser)
?: return if (isApiCancelled) null else result
?: return if (isApiCancelled()) null else result
if (bodyString.isEmpty()) {
@ -641,9 +641,9 @@ class TootApiClient(
// クライアント名が一致してて
// パーミッションが同じ
tmpClientInfo != null &&
clientName == tmpClientInfo.string("name") &&
compareScopeArray(scopeArray, tmpClientInfo["permission"].cast()) &&
appSecret?.isNotEmpty() == true -> {
clientName == tmpClientInfo.string("name") &&
compareScopeArray(scopeArray, tmpClientInfo["permission"].cast()) &&
appSecret?.isNotEmpty() == true -> {
// クライアント情報を再利用する
result.data = prepareBrowserUrlMisskey(appSecret)
return result
@ -1250,7 +1250,7 @@ class TootApiClient(
val request = requestBuilder.url(url).build()
publishApiProgress(context.getString(R.string.request_api, request.method, path))
ws = httpClient.getWebSocket(request, wsListener)
if (isApiCancelled) {
if (isApiCancelled()) {
ws.cancel()
return Pair(null, null)
}

View File

@ -36,8 +36,7 @@ class ColumnTask_Gap(
ctStarted.set(true)
val client = TootApiClient(context, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = isCancelled || column.isDispose.get()
override suspend fun isApiCancelled() = isCancelled || column.isDispose.get()
override suspend fun publishApiProgress(s: String) {
runOnMainLooper {

View File

@ -32,8 +32,7 @@ class ColumnTask_Loading(
}
val client = TootApiClient(context, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = isCancelled || column.isDispose.get()
override suspend fun isApiCancelled() = isCancelled || column.isDispose.get()
override suspend fun publishApiProgress(s: String) {
runOnMainLooper {
@ -1004,7 +1003,7 @@ class ColumnTask_Loading(
// 祖先
val listAsc = ArrayList<TootStatus>()
while (true) {
if (client.isApiCancelled) return null
if (client.isApiCancelled()) return null
queryParams["offset"] = listAsc.size
result = client.request(
"/api/notes/conversation", queryParams.toPostRequestBuilder()
@ -1021,7 +1020,7 @@ class ColumnTask_Loading(
var untilId: EntityId? = null
while (true) {
if (client.isApiCancelled) return null
if (client.isApiCancelled()) return null
when {
untilId == null -> {
@ -1089,8 +1088,8 @@ class ColumnTask_Loading(
this.listTmp = ArrayList(
(conversationContext.ancestors?.size ?: 0) +
(conversationContext.descendants?.size ?: 0) +
1
(conversationContext.descendants?.size ?: 0) +
1
)
if (conversationContext.ancestors != null) {

View File

@ -33,8 +33,7 @@ class ColumnTask_Refresh(
ctStarted.set(true)
val client = TootApiClient(context, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = isCancelled || column.isDispose.get()
override suspend fun isApiCancelled() = isCancelled || column.isDispose.get()
override suspend fun publishApiProgress(s: String) {
runOnMainLooper {

View File

@ -689,7 +689,7 @@ enum class ColumnType(
loading = { client ->
val whoResult = column.loadProfileAccount(client, parser, true)
when {
client.isApiCancelled || column.whoAccount == null -> whoResult
client.isApiCancelled() || column.whoAccount == null -> whoResult
else -> column.profileTab.ct.loading(this, client)
}
},

View File

@ -395,8 +395,8 @@ class TaskRunner(
{ currentCall = WeakReference(it) }
private val client = TootApiClient(context, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = job.isJobCancelled || (suspendJob?.isCancelled == true)
override suspend fun isApiCancelled() =
job.isJobCancelled || (suspendJob?.isCancelled == true)
}).apply {
currentCallCallback = onCallCreated
}

View File

@ -30,7 +30,7 @@ object MspHelper {
var user_token: String? = PrefS.spMspUserToken(pref)
for (nTry in 0 until 3) {
if (callback.isApiCancelled) return null
if (callback.isApiCancelled()) return null
// ユーザトークンがなければ取得する
if (user_token == null || user_token.isEmpty()) {

View File

@ -1,21 +1,20 @@
package jp.juggler.subwaytooter.streaming
import android.os.SystemClock
import jp.juggler.subwaytooter.pref.PrefB
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.column.onStatusRemoved
import jp.juggler.subwaytooter.column.reloadFilter
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.*
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.net.ProtocolException
import java.net.SocketException
import java.util.ArrayList
import java.util.HashSet
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
@ -38,8 +37,7 @@ class StreamConnection(
private val isDisposed = AtomicBoolean(false)
override val isApiCancelled: Boolean
get() = isDisposed.get()
override suspend fun isApiCancelled() = isDisposed.get()
val client = TootApiClient(manager.context, callback = this)
.apply { account = acctGroup.account }
@ -270,7 +268,7 @@ class StreamConnection(
if (payload is TootNotification &&
(payload.type == TootNotification.TYPE_EMOJI_REACTION ||
payload.type == TootNotification.TYPE_EMOJI_REACTION_PLEROMA)
payload.type == TootNotification.TYPE_EMOJI_REACTION_PLEROMA)
) {
log.d("emoji_reaction (notification) ${payload.status?.id}")

View File

@ -1,12 +1,12 @@
package jp.juggler.subwaytooter.streaming
import jp.juggler.subwaytooter.AppState
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.HighlightWord
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.LogCategory
@ -37,7 +37,7 @@ class StreamManager(val appState: AppState) {
val client = TootApiClient(
appState.context,
callback = object : TootApiCallback {
override val isApiCancelled = false
override suspend fun isApiCancelled() = false
}
)

View File

@ -15,7 +15,12 @@ import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody
@ -23,8 +28,8 @@ import okhttp3.RequestBody
import okio.BufferedSink
import java.io.*
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.CancellationException
import kotlin.coroutines.coroutineContext
class AttachmentRequest(
val account: SavedAccount,
@ -32,7 +37,7 @@ class AttachmentRequest(
val uri: Uri,
val mimeType: String,
val isReply: Boolean,
val onUploadEnd: () -> Unit ={},
val onUploadEnd: () -> Unit = {},
)
class AttachmentUploader(
@ -153,19 +158,47 @@ class AttachmentUploader(
}
}
private val context: Context = contextArg.applicationContext
private val attachmentQueue = ConcurrentLinkedQueue<AttachmentRequest>()
private var attachmentWorker: AttachmentWorker? = null
private val context = contextArg.applicationContext!!
private var lastAttachmentAdd = 0L
private var lastAttachmentComplete = 0L
private var channel: Channel<AttachmentRequest>? = null
fun onActivityDestroy() {
attachmentWorker?.cancel()
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) {
try {
handleRequest(it.receive())
} catch (ex: Throwable) {
when (ex) {
is CancellationException, is ClosedReceiveChannelException -> break
else -> context.showToast(ex)
}
}
}
}
}
}
}
fun addRequest(
request: AttachmentRequest,
) {
fun onActivityDestroy() {
try {
synchronized(this) {
channel?.close()
channel = null
}
} catch (ex: Throwable) {
log.e(ex)
}
}
fun addRequest(request: AttachmentRequest) {
// アップロード開始トースト(連発しない)
val now = System.currentTimeMillis()
if (now - lastAttachmentAdd >= 5000L) {
@ -176,17 +209,237 @@ class AttachmentUploader(
// マストドンは添付メディアをID順に表示するため
// 画像が複数ある場合は一つずつ処理する必要がある
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う
attachmentQueue.add(request)
val oldWorker = attachmentWorker
if (oldWorker == null || !oldWorker.isAlive || oldWorker.isCancelled.get()) {
oldWorker?.cancel()
attachmentWorker = AttachmentWorker()
} else {
oldWorker.notifyEx()
launchIO { prepareChannel().send(request) }
}
private suspend fun handleRequest(request: AttachmentRequest) {
val result = request.upload()
withContext(Dispatchers.Main) {
handleResult(request, result)
}
}
fun handleResult(request: AttachmentRequest, result: TootApiResult?) {
@WorkerThread
private suspend fun AttachmentRequest.upload(): TootApiResult? {
if (mimeType.isEmpty()) return TootApiResult("mime_type is empty.")
try {
val client = TootApiClient(context, callback = object : TootApiCallback {
override suspend fun isApiCancelled() = !coroutineContext.isActive
})
client.account = account
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
)
)
}
}
// 設定からリサイズ指定を読む
val resizeConfig = account.getResizeConfig()
val opener = createOpener(uri, mimeType, resizeConfig)
val mediaSizeMax = when {
mimeType.startsWith("video") || mimeType.startsWith("audio") ->
account.getMovieMaxBytes(ti)
else ->
account.getImageMaxBytes(ti)
}
val contentLength = getStreamSize(true, opener.open())
if (contentLength > mediaSizeMax) {
return TootApiResult(
context.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, uri))
return if (account.isMisskey) {
val multipartBuilder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
val apiKey = account.token_info?.string(TootApiClient.KEY_API_KEY_MISSKEY)
if (apiKey?.isNotEmpty() == true) {
multipartBuilder.addFormDataPart("i", apiKey)
}
multipartBuilder.addFormDataPart(
"file",
fileName,
object : RequestBody() {
override fun contentType(): MediaType {
return opener.mimeType.toMediaType()
}
@Throws(IOException::class)
override fun contentLength(): Long {
return contentLength
}
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
opener.open().use { inData ->
val tmp = ByteArray(4096)
while (true) {
val r = inData.read(tmp, 0, tmp.size)
if (r <= 0) break
sink.write(tmp, 0, r)
}
}
}
}
)
val result = client.request(
"/api/drive/files/create",
multipartBuilder.build().toPost()
)
opener.deleteTempFile()
onUploadEnd()
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
}
}
result
} else {
suspend fun postMedia(path: String) = client.request(
path,
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"file",
fileName,
object : RequestBody() {
override fun contentType(): MediaType {
return opener.mimeType.toMediaType()
}
@Throws(IOException::class)
override fun contentLength(): Long {
return contentLength
}
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
opener.open().use { inData ->
val tmp = ByteArray(4096)
while (true) {
val r = inData.read(tmp, 0, tmp.size)
if (r <= 0) break
sink.write(tmp, 0, r)
}
}
}
}
)
.build().toPost()
)
suspend fun postV1() = postMedia("/api/v1/media")
suspend fun postV2(): TootApiResult? {
// 3.1.3未満は v1 APIを使う
if (!ti.versionGE(TootInstance.VERSION_3_1_3)) {
return postV1()
}
// 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
}
// ポーリングして処理完了を待つ
val id =
parseItem(::TootAttachment, ServiceType.MASTODON, result?.jsonObject)
?.id
?: return TootApiResult("/api/v2/media did not return the media ID.")
var lastResponse = SystemClock.elapsedRealtime()
loop@ while (true) {
delay(1000L)
val r2 = client.request("/api/v1/media/$id")
?: return null // cancelled
val now = SystemClock.elapsedRealtime()
when (r2.response?.code) {
// complete,or 4xx error
200, in 400 until 500 -> return r2
// continue to wait
206 -> lastResponse = now
// too many temporary error without 206 response.
else -> if (now - lastResponse >= 120000L) {
return TootApiResult("timeout.")
}
}
}
}
val result = postV2()
opener.deleteTempFile()
onUploadEnd()
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
}
}
result
}
} catch (ex: Throwable) {
return TootApiResult(ex.withCaption("read failed."))
}
}
private fun handleResult(request: AttachmentRequest, result: TootApiResult?) {
val pa = request.pa
pa.status = when (pa.attachment) {
null -> {
@ -213,249 +466,7 @@ class AttachmentUploader(
pa.callback?.onPostAttachmentComplete(pa)
}
inner class AttachmentWorker : WorkerBase() {
internal val isCancelled = AtomicBoolean(false)
override fun cancel() {
isCancelled.set(true)
notifyEx()
}
override suspend fun run() {
try {
while (!isCancelled.get()) {
val request = attachmentQueue.poll()
if (request == null) {
waitEx(86400000L)
continue
}
val result = request.upload()
handler.post { handleResult(request, result) }
}
} catch (ex: Throwable) {
log.trace(ex)
log.e(ex, "AttachmentWorker")
}
}
@WorkerThread
private suspend fun AttachmentRequest.upload(): TootApiResult? {
if (mimeType.isEmpty()) return TootApiResult("mime_type is empty.")
try {
val client = TootApiClient(context, callback = object : TootApiCallback {
override val isApiCancelled: Boolean
get() = isCancelled.get()
})
client.account = account
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))
}
}
// 設定からリサイズ指定を読む
val resizeConfig = account.getResizeConfig()
val opener = createOpener(uri, mimeType, resizeConfig)
val mediaSizeMax = when {
mimeType.startsWith("video") || mimeType.startsWith("audio") ->
account.getMovieMaxBytes(ti)
else ->
account.getImageMaxBytes(ti)
}
val contentLength = getStreamSize(true, opener.open())
if (contentLength > mediaSizeMax) {
return TootApiResult(
context.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, uri))
return if (account.isMisskey) {
val multipartBuilder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
val apiKey = account.token_info?.string(TootApiClient.KEY_API_KEY_MISSKEY)
if (apiKey?.isNotEmpty() == true) {
multipartBuilder.addFormDataPart("i", apiKey)
}
multipartBuilder.addFormDataPart(
"file",
fileName,
object : RequestBody() {
override fun contentType(): MediaType {
return opener.mimeType.toMediaType()
}
@Throws(IOException::class)
override fun contentLength(): Long {
return contentLength
}
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
opener.open().use { inData ->
val tmp = ByteArray(4096)
while (true) {
val r = inData.read(tmp, 0, tmp.size)
if (r <= 0) break
sink.write(tmp, 0, r)
}
}
}
}
)
val result = client.request(
"/api/drive/files/create",
multipartBuilder.build().toPost()
)
opener.deleteTempFile()
onUploadEnd()
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
}
}
result
} else {
suspend fun postMedia(path: String) = client.request(
path,
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"file",
fileName,
object : RequestBody() {
override fun contentType(): MediaType {
return opener.mimeType.toMediaType()
}
@Throws(IOException::class)
override fun contentLength(): Long {
return contentLength
}
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
opener.open().use { inData ->
val tmp = ByteArray(4096)
while (true) {
val r = inData.read(tmp, 0, tmp.size)
if (r <= 0) break
sink.write(tmp, 0, r)
}
}
}
}
)
.build().toPost()
)
suspend fun postV1() = postMedia("/api/v1/media")
suspend fun postV2(): TootApiResult? {
// 3.1.3未満は v1 APIを使う
if (!ti.versionGE(TootInstance.VERSION_3_1_3)) {
return postV1()
}
// 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
}
// ポーリングして処理完了を待つ
val id = parseItem(::TootAttachment, ServiceType.MASTODON, result?.jsonObject)
?.id
?: return TootApiResult("/api/v2/media did not return the media ID.")
var lastResponse = SystemClock.elapsedRealtime()
loop@ while (true) {
delay(1000L)
val r2 = client.request("/api/v1/media/$id")
?: return null // cancelled
val now = SystemClock.elapsedRealtime()
when (r2.response?.code) {
// complete,or 4xx error
200, in 400 until 500 -> return r2
// continue to wait
206 -> lastResponse = now
// too many temporary error without 206 response.
else -> if (now - lastResponse >= 120000L) {
return TootApiResult("timeout.")
}
}
}
}
val result = postV2()
opener.deleteTempFile()
onUploadEnd()
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
}
}
result
}
} catch (ex: Throwable) {
return TootApiResult(ex.withCaption("read failed."))
}
}
}
internal interface InputStreamOpener {
val mimeType: String
@Throws(IOException::class)
@ -538,7 +549,8 @@ class AttachmentUploader(
@Throws(IOException::class)
override fun open(): InputStream {
return context.contentResolver.openInputStream(uri) ?: error("openInputStream returns null")
return context.contentResolver.openInputStream(uri)
?: error("openInputStream returns null")
}
override fun deleteTempFile() {
@ -760,7 +772,8 @@ class AttachmentUploader(
}
.toPutRequestBuilder()
)?.also { result ->
resultAttachment = parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
resultAttachment =
parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
}
}
} catch (ex: Throwable) {

View File

@ -22,7 +22,7 @@ private object EndlessScope : CoroutineScope {
// メインスレッド上で動作するコルーチンを起動して、終了を待たずにリターンする。
// 起動されたアクティビティのライフサイクルに関わらず中断しない。
fun launchMain(block: suspend CoroutineScope.() -> Unit): Job =
EndlessScope.launch(context = Dispatchers.Main) {
EndlessScope.launch(context = Dispatchers.Main.immediate) {
try {
block()
} catch (ex: CancellationException) {

View File

@ -48,7 +48,7 @@ object ToastUtils {
fun Context.showToast(bLong: Boolean, caption: String?): Boolean =
ToastUtils.showToastImpl(this, bLong, caption ?: "(null)")
fun Context.showToast(ex: Throwable, caption: String): Boolean =
fun Context.showToast(ex: Throwable, caption: String="error."): Boolean =
ToastUtils.showToastImpl(this, true, ex.withCaption(caption))
fun Context.showToast(bLong: Boolean, stringId: Int, vararg args: Any): Boolean =