From b0ddddfe4903c7e791bafa5cbbddba96d025fd5f Mon Sep 17 00:00:00 2001 From: tateisu Date: Wed, 5 Jan 2022 13:22:44 +0900 Subject: [PATCH] =?UTF-8?q?TootApiClient=E3=81=AEisApiCancelle=E3=82=92val?= =?UTF-8?q?=20=E3=81=8B=E3=82=89=20suspend=20fun=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jp/juggler/subwaytooter/ActMediaViewer.kt | 4 +- .../subwaytooter/action/Action_Account.kt | 8 +- .../subwaytooter/actpost/ActPostDrafts.kt | 11 +- .../jp/juggler/subwaytooter/api/ApiTask.kt | 3 +- .../subwaytooter/api/TootApiCallback.kt | 2 +- .../juggler/subwaytooter/api/TootApiClient.kt | 30 +- .../subwaytooter/column/ColumnTask_Gap.kt | 3 +- .../subwaytooter/column/ColumnTask_Loading.kt | 11 +- .../subwaytooter/column/ColumnTask_Refresh.kt | 3 +- .../juggler/subwaytooter/column/ColumnType.kt | 2 +- .../subwaytooter/notification/TaskRunner.kt | 4 +- .../juggler/subwaytooter/search/MspHelper.kt | 2 +- .../streaming/StreamConnection.kt | 10 +- .../subwaytooter/streaming/StreamManager.kt | 6 +- .../subwaytooter/util/AttachmentUploader.kt | 539 +++++++++--------- .../main/java/jp/juggler/util/EndlessScope.kt | 2 +- .../main/java/jp/juggler/util/ToastUtils.kt | 2 +- 17 files changed, 325 insertions(+), 317 deletions(-) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt index 50610c54..14e3a257 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMediaViewer.kt @@ -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("?") diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt index 7045d59d..c3783a83 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostDrafts.kt b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostDrafts.kt index e7bc3312..07672bc0 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostDrafts.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostDrafts.kt @@ -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のアンケート結果は再編集の対象外 } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/ApiTask.kt b/app/src/main/java/jp/juggler/subwaytooter/api/ApiTask.kt index 2e4c6df5..966786a4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/ApiTask.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/ApiTask.kt @@ -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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiCallback.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiCallback.kt index 821a2db4..99a96499 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiCallback.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiCallback.kt @@ -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) {} } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index 6c1504c4..d77031c9 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -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) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Gap.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Gap.kt index adc24e06..bc72d205 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Gap.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Gap.kt @@ -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 { diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt index aaa82e3b..ec551e26 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Loading.kt @@ -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() 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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt index 36bac8b0..97d8f7cd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt @@ -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 { diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt index bc11b02f..788f1c2a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnType.kt @@ -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) } }, diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt index 0f52deef..3b104cd7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/TaskRunner.kt @@ -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 } diff --git a/app/src/main/java/jp/juggler/subwaytooter/search/MspHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/search/MspHelper.kt index 93e407be..af178238 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/search/MspHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/search/MspHelper.kt @@ -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()) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt index 5a4d1fd8..59649248 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt @@ -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}") diff --git a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamManager.kt b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamManager.kt index 7222e837..f50a6001 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamManager.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamManager.kt @@ -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 } ) diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt index 0e1d48a3..3329e498 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt @@ -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() - private var attachmentWorker: AttachmentWorker? = null + private val context = contextArg.applicationContext!! private var lastAttachmentAdd = 0L private var lastAttachmentComplete = 0L + private var channel: Channel? = null - fun onActivityDestroy() { - attachmentWorker?.cancel() + private fun prepareChannel(): Channel { + // double check before/after lock + channel?.let { return it } + synchronized(this) { + channel?.let { return it } + return Channel(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) { diff --git a/app/src/main/java/jp/juggler/util/EndlessScope.kt b/app/src/main/java/jp/juggler/util/EndlessScope.kt index b46446e5..59a8490c 100644 --- a/app/src/main/java/jp/juggler/util/EndlessScope.kt +++ b/app/src/main/java/jp/juggler/util/EndlessScope.kt @@ -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) { diff --git a/app/src/main/java/jp/juggler/util/ToastUtils.kt b/app/src/main/java/jp/juggler/util/ToastUtils.kt index 02dad7c7..352aafcc 100644 --- a/app/src/main/java/jp/juggler/util/ToastUtils.kt +++ b/app/src/main/java/jp/juggler/util/ToastUtils.kt @@ -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 =