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

View File

@ -344,7 +344,7 @@ suspend fun Context.accountListWithFilter(
.mapNotNull { it.await() } .mapNotNull { it.await() }
.let { accountListReorder(it, pickupHost) } .let { accountListReorder(it, pickupHost) }
} }
if (client.isApiCancelled) null else TootApiResult() if (client.isApiCancelled()) null else TootApiResult()
} }
return resultList return resultList
} }
@ -352,7 +352,7 @@ suspend fun Context.accountListWithFilter(
suspend fun ActMain.accountListCanQuote(pickupHost: Host? = null) = suspend fun ActMain.accountListCanQuote(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a -> accountListWithFilter(pickupHost) { client, a ->
when { when {
client.isApiCancelled -> false client.isApiCancelled() -> false
a.isPseudo -> false a.isPseudo -> false
a.isMisskey -> true a.isMisskey -> true
else -> { else -> {
@ -368,7 +368,7 @@ suspend fun ActMain.accountListCanQuote(pickupHost: Host? = null) =
suspend fun ActMain.accountListCanReaction(pickupHost: Host? = null) = suspend fun ActMain.accountListCanReaction(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a -> accountListWithFilter(pickupHost) { client, a ->
when { when {
client.isApiCancelled -> false client.isApiCancelled() -> false
a.isPseudo -> false a.isPseudo -> false
a.isMisskey -> true a.isMisskey -> true
else -> { else -> {
@ -384,7 +384,7 @@ suspend fun ActMain.accountListCanReaction(pickupHost: Host? = null) =
suspend fun ActMain.accountListCanSeeMyReactions(pickupHost: Host? = null) = suspend fun ActMain.accountListCanSeeMyReactions(pickupHost: Host? = null) =
accountListWithFilter(pickupHost) { client, a -> accountListWithFilter(pickupHost) { client, a ->
when { when {
client.isApiCancelled -> false client.isApiCancelled() -> false
a.isPseudo -> false a.isPseudo -> false
else -> { else -> {
val (ti, ri) = TootInstance.getEx(client.copy(), account = a) 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 fun isTaskCancelled() = !this.coroutineContext.isActive
var content = draft.string(DRAFT_CONTENT) ?: "" 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 accountDbId = draft.long(DRAFT_ACCOUNT_DB_ID) ?: -1L
val account = SavedAccount.loadAccount(this@restoreDraft, accountDbId) val account = SavedAccount.loadAccount(this@restoreDraft, accountDbId)
@ -175,8 +176,7 @@ fun ActPost.restoreDraft(draft: JsonObject) {
// アカウントがあるなら基本的にはすべての情報を復元できるはずだが、いくつか確認が必要だ // アカウントがあるなら基本的にはすべての情報を復元できるはずだが、いくつか確認が必要だ
val apiClient = TootApiClient(this@restoreDraft, callback = object : TootApiCallback { val apiClient = TootApiClient(this@restoreDraft, callback = object : TootApiCallback {
override val isApiCancelled: Boolean override suspend fun isApiCancelled() = isTaskCancelled()
get() = isTaskCancelled()
override suspend fun publishApiProgress(s: String) { override suspend fun publishApiProgress(s: String) {
progress.setMessageEx(s) progress.setMessageEx(s)
@ -236,7 +236,8 @@ fun ActPost.restoreDraft(draft: JsonObject) {
val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST) val tmpAttachmentList = draft.jsonArray(DRAFT_ATTACHMENT_LIST)
val replyId = EntityId.from(draft, DRAFT_REPLY_ID) 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) val evEmoji = DecodeOptions(this@restoreDraft, decodeEmoji = true)
.decodeEmoji(content) .decodeEmoji(content)
@ -381,7 +382,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
} }
srcEnquete.pollType == TootPollsType.FriendsNico && srcEnquete.pollType == TootPollsType.FriendsNico &&
srcEnquete.type != TootPolls.TYPE_ENQUETE -> { srcEnquete.type != TootPolls.TYPE_ENQUETE -> {
// フレニコAPIのアンケート結果は再編集の対象外 // フレニコAPIのアンケート結果は再編集の対象外
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
package jp.juggler.subwaytooter.streaming package jp.juggler.subwaytooter.streaming
import jp.juggler.subwaytooter.AppState 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.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.Acct import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.TootInstance 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.HighlightWord
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
@ -37,7 +37,7 @@ class StreamManager(val appState: AppState) {
val client = TootApiClient( val client = TootApiClient(
appState.context, appState.context,
callback = object : TootApiCallback { 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.api.runApiTask
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* 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.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import okhttp3.MediaType import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MultipartBody import okhttp3.MultipartBody
@ -23,8 +28,8 @@ import okhttp3.RequestBody
import okio.BufferedSink import okio.BufferedSink
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicBoolean import kotlin.coroutines.coroutineContext
class AttachmentRequest( class AttachmentRequest(
val account: SavedAccount, val account: SavedAccount,
@ -32,7 +37,7 @@ class AttachmentRequest(
val uri: Uri, val uri: Uri,
val mimeType: String, val mimeType: String,
val isReply: Boolean, val isReply: Boolean,
val onUploadEnd: () -> Unit ={}, val onUploadEnd: () -> Unit = {},
) )
class AttachmentUploader( class AttachmentUploader(
@ -153,19 +158,47 @@ class AttachmentUploader(
} }
} }
private val context: Context = contextArg.applicationContext private val context = contextArg.applicationContext!!
private val attachmentQueue = ConcurrentLinkedQueue<AttachmentRequest>()
private var attachmentWorker: AttachmentWorker? = null
private var lastAttachmentAdd = 0L private var lastAttachmentAdd = 0L
private var lastAttachmentComplete = 0L private var lastAttachmentComplete = 0L
private var channel: Channel<AttachmentRequest>? = null
fun onActivityDestroy() { private fun prepareChannel(): Channel<AttachmentRequest> {
attachmentWorker?.cancel() // 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( fun onActivityDestroy() {
request: AttachmentRequest, try {
) { synchronized(this) {
channel?.close()
channel = null
}
} catch (ex: Throwable) {
log.e(ex)
}
}
fun addRequest(request: AttachmentRequest) {
// アップロード開始トースト(連発しない) // アップロード開始トースト(連発しない)
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
if (now - lastAttachmentAdd >= 5000L) { if (now - lastAttachmentAdd >= 5000L) {
@ -176,17 +209,237 @@ class AttachmentUploader(
// マストドンは添付メディアをID順に表示するため // マストドンは添付メディアをID順に表示するため
// 画像が複数ある場合は一つずつ処理する必要がある // 画像が複数ある場合は一つずつ処理する必要がある
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う // 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う
attachmentQueue.add(request) launchIO { prepareChannel().send(request) }
val oldWorker = attachmentWorker }
if (oldWorker == null || !oldWorker.isAlive || oldWorker.isCancelled.get()) {
oldWorker?.cancel() private suspend fun handleRequest(request: AttachmentRequest) {
attachmentWorker = AttachmentWorker() val result = request.upload()
} else { withContext(Dispatchers.Main) {
oldWorker.notifyEx() 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 val pa = request.pa
pa.status = when (pa.attachment) { pa.status = when (pa.attachment) {
null -> { null -> {
@ -213,249 +466,7 @@ class AttachmentUploader(
pa.callback?.onPostAttachmentComplete(pa) 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 { internal interface InputStreamOpener {
val mimeType: String val mimeType: String
@Throws(IOException::class) @Throws(IOException::class)
@ -538,7 +549,8 @@ class AttachmentUploader(
@Throws(IOException::class) @Throws(IOException::class)
override fun open(): InputStream { 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() { override fun deleteTempFile() {
@ -760,7 +772,8 @@ class AttachmentUploader(
} }
.toPutRequestBuilder() .toPutRequestBuilder()
)?.also { result -> )?.also { result ->
resultAttachment = parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject) resultAttachment =
parseItem(::TootAttachment, ServiceType.MASTODON, result.jsonObject)
} }
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {

View File

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

View File

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