From f5accd5ffa9f27fc629d6ef93202e642ef848ce5 Mon Sep 17 00:00:00 2001 From: tateisu Date: Wed, 19 Jul 2023 12:31:16 +0900 Subject: [PATCH] =?UTF-8?q?BuildConfig=E3=82=92=E6=8E=92=E9=99=A4=E3=80=82?= =?UTF-8?q?=E6=B7=BB=E4=BB=98=E3=83=87=E3=83=BC=E3=82=BF=E3=81=AE=E3=82=A2?= =?UTF-8?q?=E3=83=83=E3=83=97=E3=83=AD=E3=83=BC=E3=83=89=E3=81=AB=E3=83=81?= =?UTF-8?q?=E3=83=A3=E3=83=8D=E3=83=AB=E3=82=92=EF=BC=92=E3=81=A4=E4=BD=BF?= =?UTF-8?q?=E3=81=A3=E3=81=A6=E3=81=9F=E3=81=AE=E3=82=92=E3=82=84=E3=82=81?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/modules.xml | 6 - app/build.gradle | 9 +- .../jp/juggler/subwaytooter/ReleaseType.kt | 6 + .../jp/juggler/subwaytooter/push/FcmFlavor.kt | 6 + .../juggler/subwaytooter/ActAccountSetting.kt | 3 +- .../jp/juggler/subwaytooter/ActAppSetting.kt | 2 + .../java/jp/juggler/subwaytooter/ActPost.kt | 11 - .../main/java/jp/juggler/subwaytooter/App1.kt | 38 +- .../subwaytooter/actmain/ActMainIntent.kt | 7 +- .../subwaytooter/actpost/ActPostAttachment.kt | 172 ++------ .../subwaytooter/api/auth/AuthMastodon.kt | 6 +- .../subwaytooter/api/auth/AuthMisskey10.kt | 15 +- .../subwaytooter/api/auth/AuthMisskey13.kt | 6 +- .../subwaytooter/appsetting/AppSettingItem.kt | 8 +- .../notification/NotificationChannels.kt | 26 +- .../subwaytooter/pref/LazyContextHolder.kt | 4 +- .../subwaytooter/util/AttachmentRequest.kt | 71 +++- .../subwaytooter/util/AttachmentUploader.kt | 374 ++++++++---------- .../jp/juggler/subwaytooter/util/UserAgent.kt | 31 ++ .../jp/juggler/subwaytooter/push/FcmFlavor.kt | 6 + .../jp/juggler/subwaytooter/ReleaseType.kt | 6 + .../java/jp/juggler/media/AudioTranscoder.kt | 46 ++- gradle.properties | 2 - 23 files changed, 400 insertions(+), 461 deletions(-) create mode 100644 app/src/debug/java/jp/juggler/subwaytooter/ReleaseType.kt create mode 100644 app/src/fcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt create mode 100644 app/src/main/java/jp/juggler/subwaytooter/util/UserAgent.kt create mode 100644 app/src/noFcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt create mode 100644 app/src/release/java/jp/juggler/subwaytooter/ReleaseType.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index eb749e40..4ad52896 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -4,30 +4,24 @@ - - - - - - diff --git a/app/build.gradle b/app/build.gradle index b2cb0905..c521003e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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"] } } diff --git a/app/src/debug/java/jp/juggler/subwaytooter/ReleaseType.kt b/app/src/debug/java/jp/juggler/subwaytooter/ReleaseType.kt new file mode 100644 index 00000000..a57cd066 --- /dev/null +++ b/app/src/debug/java/jp/juggler/subwaytooter/ReleaseType.kt @@ -0,0 +1,6 @@ +package jp.juggler.subwaytooter + +object ReleaseType { + const val isDebug = true + const val isRelease = !isDebug +} diff --git a/app/src/fcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt b/app/src/fcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt new file mode 100644 index 00000000..0cbbfc50 --- /dev/null +++ b/app/src/fcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt @@ -0,0 +1,6 @@ +package jp.juggler.subwaytooter.push + +object FcmFlavor { + const val APPLICATION_ID = "jp.juggler.subwaytooter" + const val CUSTOM_SCHEME = "subwaytooter" +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt index b81e013f..bf0b1182 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt @@ -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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt index 0af2d398..55e38d68 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt @@ -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() } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index cb1840f2..0ff88556 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -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(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() } diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.kt b/app/src/main/java/jp/juggler/subwaytooter/App1.kt index cfc325ab..66596583 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.kt @@ -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() } diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt index d0af7999..94c4a4c7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt @@ -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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt index e30cbba5..969ae07d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actpost/ActPostAttachment.kt @@ -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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt index 36c07f0d..72fcf02f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt @@ -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 { // 古いサーバ diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey10.kt b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey10.kt index 66e5253c..b2e8f76a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey10.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey10.kt @@ -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") diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey13.kt b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey13.kt index 24a6b591..2f76a917 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey13.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMisskey13.kt @@ -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) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt index 77e771bb..9f20badf 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppSettingItem.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/NotificationChannels.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/NotificationChannels.kt index a94dee84..9d39b11d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/NotificationChannels.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/NotificationChannels.kt @@ -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", ), ; diff --git a/app/src/main/java/jp/juggler/subwaytooter/pref/LazyContextHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/pref/LazyContextHolder.kt index babdfe3c..2b08a372 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/pref/LazyContextHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/pref/LazyContextHolder.kt @@ -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 { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentRequest.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentRequest.kt index df3c736d..f2471b95 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentRequest.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentRequest.kt @@ -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( ) } } + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt index 88caa677..f75a208b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentUploader.kt @@ -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? = null + private val channel = Channel(capacity = Channel.UNLIMITED) - private fun prepareChannel(): Channel { - // double check before/after lock - channel?.let { return it } - synchronized(this) { - channel?.let { return it } - return Channel(capacity = Channel.UNLIMITED) - .also { - channel = it - launchIO { - while (true) { - 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) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/UserAgent.kt b/app/src/main/java/jp/juggler/subwaytooter/util/UserAgent.kt new file mode 100644 index 00000000..39837327 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/UserAgent.kt @@ -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() + } +} + diff --git a/app/src/noFcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt b/app/src/noFcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt new file mode 100644 index 00000000..058f4303 --- /dev/null +++ b/app/src/noFcm/java/jp/juggler/subwaytooter/push/FcmFlavor.kt @@ -0,0 +1,6 @@ +package jp.juggler.subwaytooter.push + +object FcmFlavor { + const val APPLICATION_ID = "jp.juggler.subwaytooter.noFcm" + const val CUSTOM_SCHEME = "subwaytooternofcm" +} diff --git a/app/src/release/java/jp/juggler/subwaytooter/ReleaseType.kt b/app/src/release/java/jp/juggler/subwaytooter/ReleaseType.kt new file mode 100644 index 00000000..824ee638 --- /dev/null +++ b/app/src/release/java/jp/juggler/subwaytooter/ReleaseType.kt @@ -0,0 +1,6 @@ +package jp.juggler.subwaytooter + +object ReleaseType { + const val isDebug = false + const val isRelease = !isDebug +} diff --git a/base/src/main/java/jp/juggler/media/AudioTranscoder.kt b/base/src/main/java/jp/juggler/media/AudioTranscoder.kt index 4a4f16e6..600694ae 100644 --- a/base/src/main/java/jp/juggler/media/AudioTranscoder.kt +++ b/base/src/main/java/jp/juggler/media/AudioTranscoder.kt @@ -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() } diff --git a/gradle.properties b/gradle.properties index 17265594..f83bac7c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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