BuildConfigを排除。添付データのアップロードにチャネルを2つ使ってたのをやめる

This commit is contained in:
tateisu 2023-07-19 12:31:16 +09:00
parent 1612b7cffe
commit f5accd5ffa
23 changed files with 400 additions and 461 deletions

View File

@ -4,30 +4,24 @@
<modules>
<module fileurl="file://$PROJECT_DIR$/SubwayTooter.iml" filepath="$PROJECT_DIR$/SubwayTooter.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.iml" filepath="$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.main.iml" filepath="$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/anko/SubwayTooter.anko.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng/SubwayTooter.apng.iml" filepath="$PROJECT_DIR$/.idea/modules/apng/SubwayTooter.apng.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng/SubwayTooter.apng.main.iml" filepath="$PROJECT_DIR$/.idea/modules/apng/SubwayTooter.apng.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng/SubwayTooter.apng.test.iml" filepath="$PROJECT_DIR$/.idea/modules/apng/SubwayTooter.apng.test.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.iml" filepath="$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.main.iml" filepath="$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/apng_android/SubwayTooter.apng_android.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.main.iml" filepath="$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/SubwayTooter.app.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.iml" filepath="$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.main.iml" filepath="$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/base/SubwayTooter.base.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.iml" filepath="$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.main.iml" filepath="$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/colorpicker/SubwayTooter.colorpicker.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.main.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/emoji/SubwayTooter.emoji.unitTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.iml" filepath="$PROJECT_DIR$/.idea/modules/icon_material_symbols/SubwayTooter.icon_material_symbols.iml" />

View File

@ -41,7 +41,6 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
kotlinOptions {
@ -96,15 +95,11 @@ android {
dimension "fcmType"
versionNameSuffix "-noFcm"
applicationIdSuffix ".noFcm"
def scheme = "subwaytooternofcm"
manifestPlaceholders = [customScheme: scheme]
buildConfigField("String", "customScheme", "\"$scheme\"")
manifestPlaceholders = [customScheme: "subwaytooternofcm"]
}
fcm {
dimension "fcmType"
def scheme = "subwaytooter"
manifestPlaceholders = [customScheme: scheme]
buildConfigField("String", "customScheme", "\"$scheme\"")
manifestPlaceholders = [customScheme: "subwaytooter"]
}
}

View File

@ -0,0 +1,6 @@
package jp.juggler.subwaytooter
object ReleaseType {
const val isDebug = true
const val isRelease = !isDebug
}

View File

@ -0,0 +1,6 @@
package jp.juggler.subwaytooter.push
object FcmFlavor {
const val APPLICATION_ID = "jp.juggler.subwaytooter"
const val CUSTOM_SCHEME = "subwaytooter"
}

View File

@ -52,7 +52,6 @@ import jp.juggler.util.network.toPost
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.*
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@ -271,7 +270,7 @@ class ActAccountSetting : AppCompatActivity(),
setSwitchColor(views.root)
views.apply {
btnPushSubscriptionNotForce.vg(BuildConfig.DEBUG)
btnPushSubscriptionNotForce.vg(ReleaseType.isDebug)
imageResizeItems = SavedAccount.resizeConfigList.map {
val caption = when (it.type) {

View File

@ -691,6 +691,8 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
etEditText.inputType = item.inputType
etEditText.setText(text)
etEditText.setSelection(0, text.length)
item.showEditText.invoke(actAppSetting,views.etEditText)
}
updateErrorView()
}

View File

@ -22,7 +22,6 @@ import jp.juggler.subwaytooter.actpost.CompletionHelper
import jp.juggler.subwaytooter.actpost.FeaturedTagCache
import jp.juggler.subwaytooter.actpost.addAttachment
import jp.juggler.subwaytooter.actpost.applyMushroomText
import jp.juggler.subwaytooter.actpost.launchAddAttachmentChannelReader
import jp.juggler.subwaytooter.actpost.onPickCustomThumbnailImpl
import jp.juggler.subwaytooter.actpost.onPostAttachmentCompleteImpl
import jp.juggler.subwaytooter.actpost.openAttachment
@ -198,13 +197,6 @@ class ActPost : AppCompatActivity(),
}
}
class AddAttachmentChannelItem(
val uri: Uri,
val mimeTypeArg: String?,
)
val addAttachmentChannel = Channel<AddAttachmentChannelItem>(capacity = Channel.BUFFERED)
////////////////////////////////////////////////////////////////
override fun onCreate(savedInstanceState: Bundle?) {
@ -228,8 +220,6 @@ class ActPost : AppCompatActivity(),
progressChannel = Channel(capacity = Channel.CONFLATED)
launchAddAttachmentChannelReader()
initUI()
// 進捗表示チャネルの回収コルーチン
@ -265,7 +255,6 @@ class ActPost : AppCompatActivity(),
}
completionHelper.onDestroy()
attachmentUploader.onActivityDestroy()
addAttachmentChannel.close()
super.onDestroy()
}

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Handler
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
@ -28,8 +27,8 @@ import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.CustomEmojiCache
import jp.juggler.subwaytooter.util.CustomEmojiLister
import jp.juggler.subwaytooter.util.ProgressResponseBody
import jp.juggler.subwaytooter.util.getUserAgent
import jp.juggler.util.*
import jp.juggler.util.data.asciiPattern
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.initializeToastUtils
@ -134,22 +133,10 @@ class App1 : Application() {
// return maxSize * 1024;
// }
val reNotAllowedInUserAgent = "[^\\x21-\\x7e]+".asciiPattern()
private var cookieManager: CookieManager? = null
private var cookieJar: CookieJar? = null
val userAgentDefault =
"SubwayTooter/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE}"
private fun getUserAgent(): String {
val userAgentCustom = PrefS.spUserAgent.value
return when {
userAgentCustom.isNotEmpty() && !reNotAllowedInUserAgent.matcher(userAgentCustom)
.find() -> userAgentCustom
else -> userAgentDefault
}
}
private fun userAgentInterceptor() =
private fun Context.userAgentInterceptor() =
Interceptor { chain ->
chain.proceed(
chain.request().newBuilder()
@ -158,17 +145,14 @@ class App1 : Application() {
)
}
private var cookieManager: CookieManager? = null
private var cookieJar: CookieJar? = null
private fun prepareOkHttp(
private fun Context.prepareOkHttp(
timeoutSecondsConnect: Int,
timeoutSecondsRead: Int,
): OkHttpClient.Builder {
Logger.getLogger(OkHttpClient::class.java.name).level = Level.FINE
var cookieJar = this.cookieJar
var cookieJar = this@Companion.cookieJar
if (cookieJar == null) {
val cookieManager = CookieManager().apply {
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
@ -176,8 +160,8 @@ class App1 : Application() {
CookieHandler.setDefault(cookieManager)
cookieJar = JavaNetCookieJar(cookieManager)
this.cookieManager = cookieManager
this.cookieJar = cookieJar
this@Companion.cookieManager = cookieManager
this@Companion.cookieJar = cookieJar
}
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
@ -285,7 +269,7 @@ class App1 : Application() {
val apiReadTimeout = max(3, PrefS.spApiReadTimeout.toInt())
// API用のHTTP設定はキャッシュを使わない
ok_http_client = prepareOkHttp(apiReadTimeout, apiReadTimeout)
ok_http_client = appContext.prepareOkHttp(apiReadTimeout, apiReadTimeout)
.build()
// ディスクキャッシュ
@ -293,14 +277,14 @@ class App1 : Application() {
val cache = Cache(cacheDir, 30000000L)
// カスタム絵文字用のHTTP設定はキャッシュを使う
ok_http_client2 = prepareOkHttp(apiReadTimeout, apiReadTimeout)
ok_http_client2 = appContext.prepareOkHttp(apiReadTimeout, apiReadTimeout)
.cache(cache)
.build()
// 内蔵メディアビューア用のHTTP設定はタイムアウトを調整可能
val mediaReadTimeout = max(3, PrefS.spMediaReadTimeout.toInt())
ok_http_client_media_viewer =
prepareOkHttp(mediaReadTimeout, mediaReadTimeout)
appContext.prepareOkHttp(mediaReadTimeout, mediaReadTimeout)
.cache(cache)
.build()
}

View File

@ -7,7 +7,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.conversationOtherInstance
import jp.juggler.subwaytooter.action.openActPostImpl
@ -32,6 +31,7 @@ import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.notification.recycleClickedNotification
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.push.fcmHandler
import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.SavedAccount
@ -56,7 +56,7 @@ fun ActMain.handleIntentUri(uri: Uri) {
try {
log.i("handleIntentUri $uri")
when (uri.scheme) {
BuildConfig.customScheme -> handleCustomSchemaUri(uri)
FcmFlavor.CUSTOM_SCHEME -> handleCustomSchemaUri(uri)
else -> handleOtherUri(uri)
}
} catch (ex: Throwable) {
@ -193,7 +193,7 @@ private fun ActMain.handleCustomSchemaUri(uri: Uri) = launchAndShowError {
// subwaytooter://oauth(\d*)/?...
handleOAuth2Callback(uri)
} else {
// ${BuildConfig.customScheme}://notification_click/?db_id=(db_id)
// ${FcmFlavor.customScheme}://notification_click/?db_id=(db_id)
handleNotificationClick(uri, dataIdString)
}
}
@ -377,6 +377,7 @@ suspend fun ActMain.updatePushDistributer() {
selectPushDistributor()
// 選択しなかった場合は購読の更新を行わない
}
else -> {
runInProgress(cancellable = false) { reporter ->
withContext(AppDispatchers.DEFAULT) {

View File

@ -8,16 +8,12 @@ import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.api.entity.TootAttachmentType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.parseItem
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound
@ -28,9 +24,7 @@ import jp.juggler.subwaytooter.dialog.focusPointDialog
import jp.juggler.subwaytooter.dialog.showTextInputDialog
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.AttachmentUploader
import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.util.resolveMimeType
import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain
@ -46,9 +40,6 @@ import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPutRequestBuilder
import jp.juggler.util.ui.isLiveActivity
import jp.juggler.util.ui.vg
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import java.nio.channels.ClosedChannelException
import kotlin.math.min
private val log = LogCategory("ActPostAttachment")
@ -132,140 +123,45 @@ fun ActPost.addAttachment(
uri: Uri,
mimeTypeArg: String? = null,
) {
val item = ActPost.AddAttachmentChannelItem(uri = uri, mimeTypeArg = mimeTypeArg)
for (nTry in 1..10) {
try {
val channelResult = addAttachmentChannel.trySend(item)
when {
channelResult.isSuccess -> return
channelResult.isClosed -> return
channelResult.isFailure -> continue
}
} catch (ex: Throwable) {
log.e(ex, "addAttachmentChannel.trySend failed.")
continue
}
}
}
suspend fun ActPost.getInstance(): TootInstance {
val client = TootApiClient(
context = applicationContext,
callback = object : TootApiCallback {
override suspend fun isApiCancelled() = isFinishing || isDestroyed
}
).apply {
this.account = this@getInstance.account
}
val (instance, ri) = TootInstance.get(client = client)
if (instance != null) return instance
when (ri) {
null -> throw CancellationException()
else -> error("missing instance information. ${ri.error}")
}
}
suspend fun ActPost.addAttachmentSuspend(
uri: Uri,
mimeTypeArg: String? = null,
) {
val actPost = this
val account = this.account
val mimeType = uri.resolveMimeType(mimeTypeArg, this)
?.notEmpty()
val isReply = states.inReplyToId != null
when {
actPost.isFinishing || actPost.isDestroyed -> {
dialogOrToast("actPost is finishing or destroyed.")
return
}
attachmentList.size >= 4 -> {
dialogOrToast(R.string.attachment_too_many)
return
}
account == null -> {
dialogOrToast(R.string.account_select_please)
return
}
mimeType == null -> {
dialogOrToast(R.string.mime_type_missing)
return
}
if (account == null) {
dialogOrToast(R.string.account_select_please)
return
} else if (attachmentList.size >= 4) {
dialogOrToast(R.string.attachment_too_many)
return
}
val instance = getInstance()
saveAttachmentList()
val pa = PostAttachment(this)
attachmentList.add(pa)
showMediaAttachment()
when {
instance.instanceType == InstanceType.Pixelfed && isReply -> {
AttachmentUploader.log.e("pixelfed_does_not_allow_reply_with_media")
dialogOrToast(R.string.pixelfed_does_not_allow_reply_with_media)
return
}
else -> {
saveAttachmentList()
val pa = PostAttachment(this)
attachmentList.add(pa)
showMediaAttachment()
val mediaConfig = instance.configuration?.jsonObject("media_attachments")
attachmentUploader.addRequest(
AttachmentRequest(
context = applicationContext,
account = account!!,
pa = pa,
uri = uri,
mimeType = mimeType!!,
instance = instance,
mediaConfig = mediaConfig,
serverMaxSqPixel = mediaConfig?.int("image_matrix_limit")?.takeIf { it > 0 },
imageResizeConfig = account.getResizeConfig(),
maxBytesVideo = min(
account.getMovieMaxBytes(instance),
mediaConfig?.int("video_size_limit")
?.takeIf { it > 0 } ?: Int.MAX_VALUE,
),
maxBytesImage = min(
account.getImageMaxBytes(instance),
mediaConfig?.int("image_size_limit")
?.takeIf { it > 0 } ?: Int.MAX_VALUE,
),
// onUploadEnd = onUploadEnd
attachmentUploader.addRequest(
AttachmentRequest(
context = applicationContext,
account = account,
pa = pa,
uri = uri,
mimeTypeArg = mimeTypeArg,
isReply = states.inReplyToId != null,
imageResizeConfig = account.getResizeConfig(),
maxBytesVideo = { instance, mediaConfig ->
min(
account.getMovieMaxBytes(instance),
mediaConfig?.int("video_size_limit")
?.takeIf { it > 0 } ?: Int.MAX_VALUE,
)
)
}
}
}
fun ActPost.launchAddAttachmentChannelReader() {
launchMain {
while (true) {
try {
val item = addAttachmentChannel.receive()
addAttachmentSuspend(item.uri, item.mimeTypeArg)
} catch (ex: Throwable) {
when (ex) {
is CancellationException -> {
log.i("launchAddAttachmentChannelReader: cancelled.")
break
}
is ClosedChannelException, is ClosedReceiveChannelException -> {
log.i("launchAddAttachmentChannelReader: channel closed.")
break
}
else ->
log.e(ex,"launchAddAttachmentChannelReader: addAttachmentSuspend raise error. retry…")
}
}
}
}
},
maxBytesImage = { instance, mediaConfig ->
min(
account.getImageMaxBytes(instance),
mediaConfig?.int("image_size_limit")
?.takeIf { it > 0 } ?: Int.MAX_VALUE,
)
},
)
)
}
fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.api.auth
import android.net.Uri
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.api.SendException
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
@ -9,6 +8,7 @@ import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
@ -23,10 +23,10 @@ class AuthMastodon(override val client: TootApiClient) : AuthBase() {
companion object {
private val log = LogCategory("MastodonAuth")
@Suppress("MayBeConstant")
@Suppress("MayBeConstant", "RedundantSuppression")
val DEBUG_AUTH = false
const val callbackUrl = "${BuildConfig.customScheme}://oauth/"
const val callbackUrl = "${FcmFlavor.CUSTOM_SCHEME}://oauth/"
fun mastodonScope(ti: TootInstance?) = when {
// 古いサーバ

View File

@ -2,7 +2,6 @@ package jp.juggler.subwaytooter.api.auth
import android.net.Uri
import androidx.core.net.toUri
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
@ -11,16 +10,25 @@ import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.*
import jp.juggler.util.data.JsonArray
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonArray
import jp.juggler.util.data.cast
import jp.juggler.util.data.digestSHA256
import jp.juggler.util.data.encodeHexLower
import jp.juggler.util.data.encodeUTF8
import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
class AuthMisskey10(override val client: TootApiClient) : AuthBase() {
companion object {
private val log = LogCategory("MisskeyOldAuth")
private const val callbackUrl = "${BuildConfig.customScheme}://misskey/auth_callback"
private const val callbackUrl = "${FcmFlavor.CUSTOM_SCHEME}://misskey/auth_callback"
fun isCallbackUrl(uriStr: String) =
uriStr.startsWith(callbackUrl) ||
@ -213,7 +221,6 @@ class AuthMisskey10(override val client: TootApiClient) : AuthBase() {
linkHelper = LinkHelper.create(ti)
)
@Suppress("UNUSED_VARIABLE")
val clientInfo = daoClientInfo.load(apiHost, clientName)
?.notEmpty() ?: error("missing client id")

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.api.auth
import android.net.Uri
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser
@ -11,12 +10,13 @@ import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import java.util.*
import java.util.UUID
/**
* miauth と呼ばれている認証手順
@ -26,7 +26,7 @@ class AuthMisskey13(override val client: TootApiClient) : AuthBase() {
companion object {
private val log = LogCategory("MisskeyMiAuth")
private const val appIconUrl = "https://m1j.zzz.ac/subwaytooter-miauth-icon.png"
private const val callbackUrl = "${BuildConfig.customScheme}://miauth/auth_callback"
private const val callbackUrl = "${FcmFlavor.CUSTOM_SCHEME}://miauth/auth_callback"
fun isCallbackUrl(uriStr: String) = uriStr.startsWith(callbackUrl)
}

View File

@ -4,6 +4,7 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.os.Build
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.Spinner
import android.widget.TextView
@ -36,6 +37,8 @@ import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.sortedByNickname
import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.subwaytooter.util.reNotAllowedInUserAgent
import jp.juggler.subwaytooter.util.userAgentDefault
import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.data.cast
import jp.juggler.util.data.intentOpenDocument
@ -111,6 +114,7 @@ class AppSettingItem(
var onClickEdit: ActAppSetting.() -> Unit = {}
var onClickReset: ActAppSetting.() -> Unit = {}
var showTextView: ActAppSetting.(TextView) -> Unit = {}
var showEditText: ActAppSetting.(EditText) -> Unit = {}
// for EditText
var hint: String? = null
@ -346,10 +350,10 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
text(PrefS.spClientName, R.string.client_name, InputTypeEx.text)
text(PrefS.spUserAgent, R.string.user_agent, InputTypeEx.textMultiLine) {
hint = App1.userAgentDefault
showEditText = { it.hint = userAgentDefault() }
filter = { it.replace(ActAppSetting.reLinefeed, " ").trim() }
getError = {
val m = App1.reNotAllowedInUserAgent.matcher(it)
val m = reNotAllowedInUserAgent.matcher(it)
when (m.find()) {
true -> getString(R.string.user_agent_error, m.group())
else -> null

View File

@ -17,9 +17,9 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.startup.Initializer
import androidx.work.ForegroundInfo
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.pref.LazyContextInitializer
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.util.*
import jp.juggler.util.log.LogCategory
@ -56,8 +56,8 @@ enum class NotificationChannels(
notificationId = 1,
pircTap = 1,
pircDelete = 1, // uriでtapとdeleteを区別している
uriPrefixDelete = "${BuildConfig.customScheme}://notification_delete/",
uriPrefixTap = "${BuildConfig.customScheme}://notification_click/",
uriPrefixDelete = "${FcmFlavor.CUSTOM_SCHEME}://notification_delete/",
uriPrefixTap = "${FcmFlavor.CUSTOM_SCHEME}://notification_click/",
),
PullWorker(
id = "PollingForegrounder",
@ -69,8 +69,8 @@ enum class NotificationChannels(
notificationId = 2,
pircTap = 2,
pircDelete = -1,
uriPrefixDelete = "${BuildConfig.customScheme}://checker",
uriPrefixTap = "${BuildConfig.customScheme}://checker-tap",
uriPrefixDelete = "${FcmFlavor.CUSTOM_SCHEME}://checker",
uriPrefixTap = "${FcmFlavor.CUSTOM_SCHEME}://checker-tap",
),
ServerTimeout(
id = "ErrorNotification",
@ -82,8 +82,8 @@ enum class NotificationChannels(
notificationId = 3,
pircTap = 3,
pircDelete = 4,
uriPrefixDelete = "${BuildConfig.customScheme}://server-timeout",
uriPrefixTap = "${BuildConfig.customScheme}://server-timeout-tap",
uriPrefixDelete = "${FcmFlavor.CUSTOM_SCHEME}://server-timeout",
uriPrefixTap = "${FcmFlavor.CUSTOM_SCHEME}://server-timeout-tap",
),
PushMessage(
id = "PushMessage",
@ -95,8 +95,8 @@ enum class NotificationChannels(
notificationId = 5,
pircTap = 5,
pircDelete = 6,
uriPrefixDelete = "${BuildConfig.customScheme}://pushMessage",
uriPrefixTap = "${BuildConfig.customScheme}://notification_click/",
uriPrefixDelete = "${FcmFlavor.CUSTOM_SCHEME}://pushMessage",
uriPrefixTap = "${FcmFlavor.CUSTOM_SCHEME}://notification_click/",
),
Alert(
id = "Alert",
@ -108,8 +108,8 @@ enum class NotificationChannels(
notificationId = 7,
pircTap = 7,
pircDelete = 8,
uriPrefixDelete = "${BuildConfig.customScheme}://alert",
uriPrefixTap = "${BuildConfig.customScheme}://alert-tap",
uriPrefixDelete = "${FcmFlavor.CUSTOM_SCHEME}://alert",
uriPrefixTap = "${FcmFlavor.CUSTOM_SCHEME}://alert-tap",
),
PushWorker(
id = "PushMessageWorker",
@ -121,8 +121,8 @@ enum class NotificationChannels(
notificationId = 9,
pircTap = 9,
pircDelete = 10,
uriPrefixDelete = "${BuildConfig.customScheme}://pushWorker",
uriPrefixTap = "${BuildConfig.customScheme}://pushWorker-tag",
uriPrefixDelete = "${FcmFlavor.CUSTOM_SCHEME}://pushWorker",
uriPrefixTap = "${FcmFlavor.CUSTOM_SCHEME}://pushWorker-tag",
),
;

View File

@ -4,7 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import androidx.startup.Initializer
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.util.os.applicationContextSafe
import java.util.concurrent.atomic.AtomicReference
@ -21,7 +21,7 @@ val lazyPref
?: LazyContextHolder.prefNullable
?: error("LazyContextHolder not initialized")
const val FILE_PROVIDER_AUTHORITY = "${BuildConfig.APPLICATION_ID}.FileProvider"
const val FILE_PROVIDER_AUTHORITY = "${FcmFlavor.APPLICATION_ID}.FileProvider"
@SuppressLint("StaticFieldLeak")
object LazyContextHolder {

View File

@ -7,11 +7,14 @@ import android.os.Build
import jp.juggler.media.generateTempFile
import jp.juggler.media.transcodeAudio
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiCallback
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.getStreamSize
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx
import jp.juggler.util.media.ResizeConfig
@ -20,6 +23,7 @@ import jp.juggler.util.media.createResizedBitmap
import jp.juggler.util.media.transcodeVideo
import java.io.File
import java.io.FileOutputStream
import java.util.concurrent.CancellationException
import kotlin.math.min
class AttachmentRequest(
@ -27,13 +31,11 @@ class AttachmentRequest(
val account: SavedAccount,
val pa: PostAttachment,
val uri: Uri,
var mimeType: String,
var mimeTypeArg: String?,
val imageResizeConfig: ResizeConfig,
val serverMaxSqPixel: Int?,
val instance: TootInstance,
val mediaConfig: JsonObject?,
val maxBytesVideo: Int,
val maxBytesImage: Int,
val maxBytesVideo: (instance: TootInstance, mediaConfig: JsonObject?) -> Int,
val maxBytesImage: (instance: TootInstance, mediaConfig: JsonObject?) -> Int,
val isReply: Boolean = false,
) {
companion object {
private val log = LogCategory("AttachmentRequest")
@ -51,14 +53,49 @@ class AttachmentRequest(
"audio/x-wav",
"audio/3gpp",
)
// val badAudioType = setOf(
// val badAudioType = setOf(
// "audio/mpeg","audio/aac",
// "audio/m4a","audio/x-m4a","audio/mp4",
// "video/x-ms-asf",
// )
private suspend fun Context.getInstance(account: SavedAccount): TootInstance {
val client = TootApiClient(
context = this,
callback = object : TootApiCallback {
override suspend fun isApiCancelled() = false
}
).apply {
this.account = account
}
val (instance, ri) = TootInstance.get(client = client)
if (instance != null) return instance
when (ri) {
null -> throw CancellationException()
else -> error("missing instance information. ${ri.error}")
}
}
}
private var _instance: TootInstance? = null
suspend fun instance(): TootInstance {
_instance?.let { return it }
return context.getInstance(account).also { _instance = it }
}
suspend fun mediaConfig(): JsonObject? =
instance().configuration?.jsonObject("media_attachments")
private suspend fun serverMaxSqPixel(): Int? =
mediaConfig()?.int("image_matrix_limit")?.takeIf { it > 0 }
val mimeType
get() = uri.resolveMimeType(mimeTypeArg, context)?.notEmpty()
?: error(context.getString(R.string.mime_type_missing))
suspend fun createOpener(): InputStreamOpener {
val mimeType = this.mimeType
// GIFはそのまま投げる
if (mimeType == MIME_TYPE_GIF) {
@ -128,10 +165,12 @@ class AttachmentRequest(
)
}
private fun createResizedImageOpener(): InputStreamOpener {
private suspend fun createResizedImageOpener(): InputStreamOpener {
try {
pa.progress = context.getString(R.string.attachment_handling_compress)
val instance = instance()
val canUseWebP = try {
PrefB.bpUseWebP.value && MIME_TYPE_WEBP.mimeTypeIsSupported(instance)
} catch (ex: Throwable) {
@ -154,7 +193,7 @@ class AttachmentRequest(
uri,
imageResizeConfig,
canSkip = canUseOriginal,
serverMaxSqPixel = serverMaxSqPixel
serverMaxSqPixel = serverMaxSqPixel()
)?.let { bitmap ->
try {
return bitmap.compressAutoType(canUseWebP)
@ -273,14 +312,16 @@ class AttachmentRequest(
val tempFile = File(cacheDir, "movie." + Thread.currentThread().id + ".tmp")
val outFile = File(cacheDir, "movie." + Thread.currentThread().id + ".mp4")
var resultFile: File? = null
try {
// 入力ファイルをコピーする
(context.contentResolver.openInputStream(uri)
?: error("openInputStream returns null.")).use { inStream ->
FileOutputStream(tempFile).use { inStream.copyTo(it) }
}
val mediaConfig = mediaConfig()
// 動画のメタデータを調べる
val info = tempFile.videoInfo
@ -330,11 +371,13 @@ class AttachmentRequest(
}
}
private suspend fun createResizedAudioOpener(srcBytes: Long): InputStreamOpener =
when {
private suspend fun createResizedAudioOpener(srcBytes: Long): InputStreamOpener {
val instance = instance()
val mediaConfig = mediaConfig()
return when {
mimeType.mimeTypeIsSupported(instance) &&
goodAudioType.contains(mimeType) &&
srcBytes <= maxBytesVideo -> contentUriOpener(
srcBytes <= maxBytesVideo(instance,mediaConfig).toLong() -> contentUriOpener(
context.contentResolver,
uri,
mimeType,
@ -360,4 +403,6 @@ class AttachmentRequest(
)
}
}
}
}

View File

@ -10,6 +10,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment
@ -35,13 +36,14 @@ import jp.juggler.util.network.toPost
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.network.toPut
import jp.juggler.util.network.toPutRequestBuilder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import okhttp3.MultipartBody
import java.util.concurrent.CancellationException
import java.nio.channels.ClosedChannelException
import kotlin.coroutines.coroutineContext
class AttachmentUploader(
@ -55,130 +57,137 @@ class AttachmentUploader(
private val safeContext = activity.applicationContext!!
private var lastAttachmentAdd = 0L
private var lastAttachmentComplete = 0L
private var channel: Channel<AttachmentRequest>? = null
private val channel = Channel<AttachmentRequest>(capacity = Channel.UNLIMITED)
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) {
val request = try {
it.receive()
} catch (ex: Throwable) {
when (ex) {
is CancellationException, is ClosedReceiveChannelException -> break
else -> {
safeContext.showToast(ex)
continue
}
}
init {
launchIO {
while (true) {
try {
val request = channel.receive()
if (request.pa.isCancelled) continue
withContext(AppDispatchers.MainImmediate) {
val pa = request.pa
pa.status = try {
withContext(pa.job + AppDispatchers.IO) {
request.upload()
}
val result = try {
if (request.pa.isCancelled) continue
withContext(request.pa.job + AppDispatchers.IO) {
request.upload()
}
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("upload failed."))
request.pa.progress = ""
val now = System.currentTimeMillis()
if (now - lastAttachmentComplete >= 5000L) {
safeContext.showToast(false, R.string.attachment_uploaded)
}
try {
request.pa.progress = ""
withContext(AppDispatchers.MainImmediate) {
handleResult(request, result)
}
} catch (ex: Throwable) {
when (ex) {
is CancellationException, is ClosedReceiveChannelException -> break
else -> {
safeContext.showToast(ex)
continue
}
}
lastAttachmentComplete = now
PostAttachment.Status.Ok
} catch (ex: Throwable) {
if (ex is CancellationException) {
// キャンセルはメッセージを出さない
} else if (ex.message?.contains("cancel", ignoreCase = true) == true) {
// キャンセルはメッセージを出さない
} else if (ex is IllegalStateException) {
safeContext.showToast(true, "${ex.message}")
} else {
safeContext.showToast(true, ex.withCaption("upload failed."))
}
PostAttachment.Status.Error
}
// 投稿中に画面回転があった場合、新しい画面のコールバックを呼び出す必要がある
pa.callback?.onPostAttachmentComplete(pa)
}
} catch (ex: Throwable) {
when (ex) {
is CancellationException -> {
log.i("AttachmentUploader: channel cancelled.")
break
}
is ClosedChannelException, is ClosedReceiveChannelException -> {
log.i("AttachmentUploader: channel closed.")
break
}
else -> safeContext.showToast(ex)
}
}
}
}
}
fun onActivityDestroy() {
try {
synchronized(this) {
channel?.close()
channel = null
}
} catch (ex: Throwable) {
log.e(ex, "can't close channel.")
channel.close()
} catch (ignored: Throwable) {
}
}
fun addRequest(request: AttachmentRequest) {
request.pa.progress = safeContext.getString(R.string.attachment_handling_start)
// アップロード開始トースト(連発しない)
val now = System.currentTimeMillis()
if (now - lastAttachmentAdd >= 5000L) {
safeContext.showToast(false, R.string.attachment_uploading)
}
lastAttachmentAdd = now
// マストドンは添付メディアをID順に表示するため
// 画像が複数ある場合は一つずつ処理する必要がある
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う
launchIO { prepareChannel().send(request) }
// 投稿画面ごとに作成したチャネルにsendして、受け側は順次処理する
launchIO {
try {
val now = System.currentTimeMillis()
// アップロード開始トースト(連発しない)
if (now - lastAttachmentAdd >= 5000L) {
safeContext.showToast(false, R.string.attachment_uploading)
}
lastAttachmentAdd = now
channel.send(request)
} catch (ex: Throwable) {
log.e(ex, "addRequest failed.")
}
}
}
@WorkerThread
private suspend fun AttachmentRequest.upload(): TootApiResult? {
private suspend fun AttachmentRequest.upload() {
val account = this.account
val instance = this.instance()
val mediaConfig = this.mediaConfig()
// ensure mimeType
this.mimeType
if (instance.instanceType == InstanceType.Pixelfed && isReply) {
error(safeContext.getString(R.string.pixelfed_does_not_allow_reply_with_media))
}
val client = TootApiClient(safeContext, callback = object : TootApiCallback {
override suspend fun isApiCancelled() = !coroutineContext.isActive
})
client.account = account
client.currentCallCallback = {}
// 入力データの変換など
val opener = this.createOpener()
try {
if (mimeType.isEmpty()) return TootApiResult("mime_type is empty.")
val client = TootApiClient(safeContext, callback = object : TootApiCallback {
override suspend fun isApiCancelled() = !coroutineContext.isActive
})
client.account = account
client.currentCallCallback = {}
val (ti, tiResult) = TootInstance.get(client)
ti ?: return tiResult
// 入力データの変換など
val opener = this.createOpener()
val maxBytes = when (opener.isImage) {
true -> maxBytesImage
else -> maxBytesVideo
true -> maxBytesImage(instance, mediaConfig)
else -> maxBytesVideo(instance, mediaConfig)
}
if (opener.contentLength > maxBytes) {
return TootApiResult(
safeContext.getString(R.string.file_size_too_big, maxBytes / 1_000_000)
)
}
if (!opener.mimeType.mimeTypeIsSupported(instance)) {
return TootApiResult(
safeContext.getString(R.string.mime_type_not_acceptable, opener.mimeType)
)
if (opener.contentLength > maxBytes.toLong()) {
error(safeContext.getString(R.string.file_size_too_big, maxBytes / 1_000_000))
} else if (!opener.mimeType.mimeTypeIsSupported(instance)) {
error(safeContext.getString(R.string.mime_type_not_acceptable, opener.mimeType))
}
val fileName = fixDocumentName(getDocumentName(safeContext.contentResolver, uri))
pa.progress = safeContext.getString(R.string.attachment_handling_uploading, 0)
fun writeProgress(percent: Int) {
if (percent < 100) {
pa.progress =
safeContext.getString(R.string.attachment_handling_uploading, percent)
pa.progress = if (percent < 100) {
safeContext.getString(R.string.attachment_handling_uploading, percent)
} else {
pa.progress = safeContext.getString(R.string.attachment_handling_waiting)
safeContext.getString(R.string.attachment_handling_waiting)
}
}
return if (account.isMisskey) {
if (account.isMisskey) {
val multipartBuilder = MultipartBody.Builder()
.setType(MultipartBody.FORM)
@ -199,16 +208,13 @@ class AttachmentUploader(
)
opener.deleteTempFile()
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val a = parseItem(jsonObject) { tootAttachment(ServiceType.MISSKEY, it) }
if (a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
}
result
result ?: throw CancellationException()
val jsonObject = result.jsonObject
?: error(result.error ?: "missing error detail")
pa.attachment =
parseItem(jsonObject) { tootAttachment(ServiceType.MISSKEY, it) }
?: error("TootAttachment.parse failed")
} else {
suspend fun postMedia(path: String) = client.request(
path,
@ -226,13 +232,14 @@ class AttachmentUploader(
suspend fun postV2(): TootApiResult? {
// 3.1.3未満は v1 APIを使う
if (!ti.versionGE(TootInstance.VERSION_3_1_3)) {
if (!instance.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
?: throw CancellationException()
val code = result.response?.code // complete,or 4xx error
when {
// 404ならv1 APIにフォールバック
code == 404 -> return postV1()
@ -242,18 +249,17 @@ class AttachmentUploader(
// ポーリングして処理完了を待つ
pa.progress = safeContext.getString(R.string.attachment_handling_waiting_async)
val id = parseItem(result?.jsonObject) {
val id = parseItem(result.jsonObject) {
tootAttachment(ServiceType.MASTODON, it)
}?.id
?: return TootApiResult("/api/v2/media did not return the media ID.")
}?.id ?: error("/api/v2/media did not return the media ID.")
var lastResponse = SystemClock.elapsedRealtime()
loop@ while (true) {
while (true) {
delay(1000L)
val r2 = client.request("/api/v1/media/$id")
?: return null // cancelled
?: throw CancellationException()
val now = SystemClock.elapsedRealtime()
when (r2.response?.code) {
@ -264,29 +270,22 @@ class AttachmentUploader(
206 -> lastResponse = now
// temporary errors, check timeout without 206 response.
else -> if (now - lastResponse >= 120000L) {
return TootApiResult("timeout.")
}
else -> if (now - lastResponse >= 120000L) error("timeout.")
}
}
}
val result = postV2()
opener.deleteTempFile()
?: throw CancellationException()
val jsonObject = result?.jsonObject
if (jsonObject != null) {
when (val a = parseItem(jsonObject) {
tootAttachment(ServiceType.MASTODON, it)
}) {
null -> result.error = "TootAttachment.parse failed"
else -> pa.attachment = a
}
}
result
val jsonObject = result.jsonObject
?: error(result.error ?: "missing error detail")
pa.attachment = parseItem(jsonObject) { tootAttachment(ServiceType.MASTODON, it) }
?: error("TootAttachment.parse failed")
}
} catch (ex: Throwable) {
return TootApiResult(ex.withCaption("read failed."))
} finally {
opener.deleteTempFile()
}
}
@ -306,38 +305,6 @@ class AttachmentUploader(
return sb.toString()
}
private fun handleResult(request: AttachmentRequest, result: TootApiResult?) {
val pa = request.pa
pa.status = when (pa.attachment) {
null -> {
if (result != null) {
when {
// キャンセルはトーストを出さない
result.error?.contains("cancel", ignoreCase = true) == true -> Unit
else -> safeContext.showToast(
true,
"${result.error} ${result.response?.request?.method} ${result.response?.request?.url}"
)
}
}
PostAttachment.Status.Error
}
else -> {
val now = System.currentTimeMillis()
if (now - lastAttachmentComplete >= 5000L) {
safeContext.showToast(false, R.string.attachment_uploaded)
}
lastAttachmentComplete = now
PostAttachment.Status.Ok
}
}
// 投稿中に画面回転があった場合、新しい画面のコールバックを呼び出す必要がある
pa.callback?.onPostAttachmentComplete(pa)
}
///////////////////////////////////////////////////////////////
// 添付データのカスタムサムネイル
suspend fun uploadCustomThumbnail(
@ -346,67 +313,62 @@ class AttachmentUploader(
pa: PostAttachment,
): TootApiResult? = try {
safeContext.runApiTask(account) { client ->
val (ti, ri) = TootInstance.get(client)
ti ?: return@runApiTask ri
val mimeType = src.uri.resolveMimeType(src.mimeType, safeContext)
if (mimeType.isNullOrEmpty()) {
return@runApiTask TootApiResult(safeContext.getString(R.string.mime_type_missing))
}
val mediaConfig = ti.configuration?.jsonObject("media_attachments")
val ar = AttachmentRequest(
context = safeContext,
account = account,
pa = pa,
uri = src.uri,
mimeType = mimeType,
instance = ti,
mediaConfig = mediaConfig,
mimeTypeArg = src.mimeType,
imageResizeConfig = ResizeConfig(ResizeType.SquarePixel, 400),
serverMaxSqPixel = mediaConfig?.int("image_matrix_limit")?.takeIf { it > 0 },
maxBytesImage = 1000000,
maxBytesVideo = 1000000,
maxBytesImage = { _, _ -> 1000000 },
maxBytesVideo = { _, _ -> 1000000 },
)
val instance = ar.instance()
val mediaConfig = ar.mediaConfig()
val maxBytesImage = ar.maxBytesImage(instance, mediaConfig)
val opener = ar.createOpener()
if (opener.contentLength > ar.maxBytesImage) {
return@runApiTask TootApiResult(
getString(
R.string.file_size_too_big,
ar.maxBytesImage / 1000000
)
)
}
try{
val fileName = fixDocumentName(getDocumentName(safeContext.contentResolver, src.uri))
if (account.isMisskey) {
opener.deleteTempFile()
TootApiResult("custom thumbnail is not supported on misskey account.")
} else {
val result = client.request(
"/api/v1/media/${pa.attachment?.id}",
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"thumbnail",
fileName,
opener.toRequestBody(),
if (opener.contentLength > maxBytesImage.toLong()) {
return@runApiTask TootApiResult(
getString(
R.string.file_size_too_big,
maxBytesImage / 1000000
)
.build().toPut()
)
opener.deleteTempFile()
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val a = parseItem(jsonObject) { tootAttachment(ServiceType.MASTODON, it) }
if (a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
)
}
result
val fileName = fixDocumentName(getDocumentName(safeContext.contentResolver, src.uri))
if (account.isMisskey) {
TootApiResult("custom thumbnail is not supported on misskey account.")
} else {
val result = client.request(
"/api/v1/media/${pa.attachment?.id}",
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"thumbnail",
fileName,
opener.toRequestBody(),
)
.build().toPut()
)
val jsonObject = result?.jsonObject
if (jsonObject != null) {
val a = parseItem(jsonObject) { tootAttachment(ServiceType.MASTODON, it) }
if (a == null) {
result.error = "TootAttachment.parse failed"
} else {
pa.attachment = a
}
}
result
}
}finally{
opener.deleteTempFile()
}
}
} catch (ex: Throwable) {

View File

@ -0,0 +1,31 @@
package jp.juggler.subwaytooter.util
import android.content.Context
import android.os.Build
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.util.data.asciiPattern
import jp.juggler.util.getPackageInfoCompat
val reNotAllowedInUserAgent = "[^\\x21-\\x7e]+".asciiPattern()
fun Context.userAgentDefault(): String {
val versionName = try {
packageManager.getPackageInfoCompat(packageName)!!.versionName
} catch (ex: Throwable) {
App1.log.e(ex, "can't get versionName.")
"0.0.0"
}
return "SubwayTooter/${versionName} Android/${Build.VERSION.RELEASE}"
}
fun Context.getUserAgent(): String {
val userAgentCustom = PrefS.spUserAgent.value
return when {
userAgentCustom.isNotEmpty() && !reNotAllowedInUserAgent.matcher(userAgentCustom)
.find() -> userAgentCustom
else -> userAgentDefault()
}
}

View File

@ -0,0 +1,6 @@
package jp.juggler.subwaytooter.push
object FcmFlavor {
const val APPLICATION_ID = "jp.juggler.subwaytooter.noFcm"
const val CUSTOM_SCHEME = "subwaytooternofcm"
}

View File

@ -0,0 +1,6 @@
package jp.juggler.subwaytooter
object ReleaseType {
const val isDebug = false
const val isRelease = !isDebug
}

View File

@ -5,9 +5,11 @@ import android.net.Uri
import android.os.Looper
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.transformer.TransformationException
import androidx.media3.transformer.Composition
import androidx.media3.transformer.EditedMediaItem
import androidx.media3.transformer.ExportException
import androidx.media3.transformer.ExportResult
import androidx.media3.transformer.TransformationRequest
import androidx.media3.transformer.TransformationResult
import androidx.media3.transformer.Transformer
import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.Dispatchers
@ -57,35 +59,39 @@ suspend fun transcodeAudio(
val tmpFile = context.generateTempFile("transcodeAudio")
// Transformerは単一スレッドで処理する要件
val result: TransformationResult = withContext(Dispatchers.Main.immediate) {
val result: ExportResult = withContext(Dispatchers.Main.immediate) {
val looper = Looper.getMainLooper()
suspendCancellableCoroutine { cont ->
val transformerListener = object : Transformer.Listener {
override fun onTransformationCompleted(
inputMediaItem: MediaItem,
transformationResult: TransformationResult,
override fun onCompleted(
composition: Composition,
exportResult: ExportResult,
) {
log.i("onTransformationCompleted inputMediaItem=$inputMediaItem transformationResult=$transformationResult")
if (cont.isActive) cont.resume(transformationResult) {}
val mediaItem = composition.sequences[0].editedMediaItems[0].mediaItem
log.i("onCompleted mediaItem=$mediaItem exportResult=$exportResult")
if (cont.isActive) cont.resume(exportResult) {}
}
override fun onTransformationError(
inputMediaItem: MediaItem,
exception: TransformationException,
override fun onError(
composition: Composition,
exportResult: ExportResult,
exportException: ExportException,
) {
val mediaItem = composition.sequences[0].editedMediaItems[0].mediaItem
log.e(
exception,
"onTransformationError inputMediaItem=$inputMediaItem"
exportException,
"onError inputMediaItem=$mediaItem, exportResult=$exportResult"
)
if (cont.isActive) cont.resumeWithException(exception)
if (cont.isActive) cont.resumeWithException(exportException)
}
override fun onFallbackApplied(
inputMediaItem: MediaItem,
composition: Composition,
originalTransformationRequest: TransformationRequest,
fallbackTransformationRequest: TransformationRequest,
) {
log.i("onFallbackApplied inputMediaItem=$inputMediaItem original=$originalTransformationRequest fallback=$fallbackTransformationRequest")
val mediaItem = composition.sequences[0].editedMediaItems[0].mediaItem
log.i("onFallbackApplied mediaItem=$mediaItem original=$originalTransformationRequest fallback=$fallbackTransformationRequest")
}
}
val transformer = Transformer.Builder(context)
@ -95,11 +101,13 @@ suspend fun transcodeAudio(
.setAudioMimeType(encodeMimeType)
.build()
)
.setRemoveVideo(true)
.setRemoveAudio(false)
.addListener(transformerListener)
.build()
transformer.startTransformation(inputMediaItem, tmpFile.canonicalPath)
val editedMediaItem = EditedMediaItem.Builder(inputMediaItem).apply {
setRemoveVideo(true)
}.build()
transformer.start(editedMediaItem, tmpFile.canonicalPath)
cont.invokeOnCancellation {
transformer.cancel()
}

View File

@ -32,5 +32,3 @@ org.gradle.vfs.watch=true
android.useAndroidX=true
android.enableJetifier=true
android.debug.obsoleteApi=true
android.defaults.buildfeatures.buildconfig=true
android.enableBuildConfigAsBytecode=true