TootApiClientのisApiCancelleをval から suspend funに変更
This commit is contained in:
parent
c7ec0fa3cd
commit
b0ddddfe49
|
@ -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("?")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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のアンケート結果は再編集の対象外
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in New Issue