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