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> <modules>
<module fileurl="file://$PROJECT_DIR$/SubwayTooter.iml" filepath="$PROJECT_DIR$/SubwayTooter.iml" /> <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.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.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/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.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.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/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.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.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/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.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.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/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.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.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/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.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.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/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.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.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/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" /> <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 { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
kotlinOptions { kotlinOptions {
@ -96,15 +95,11 @@ android {
dimension "fcmType" dimension "fcmType"
versionNameSuffix "-noFcm" versionNameSuffix "-noFcm"
applicationIdSuffix ".noFcm" applicationIdSuffix ".noFcm"
def scheme = "subwaytooternofcm" manifestPlaceholders = [customScheme: "subwaytooternofcm"]
manifestPlaceholders = [customScheme: scheme]
buildConfigField("String", "customScheme", "\"$scheme\"")
} }
fcm { fcm {
dimension "fcmType" dimension "fcmType"
def scheme = "subwaytooter" manifestPlaceholders = [customScheme: "subwaytooter"]
manifestPlaceholders = [customScheme: scheme]
buildConfigField("String", "customScheme", "\"$scheme\"")
} }
} }

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,16 +8,12 @@ import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.ActPost import jp.juggler.subwaytooter.ActPost
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiTask 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.TootApiResult
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.ServiceType import jp.juggler.subwaytooter.api.entity.ServiceType
import jp.juggler.subwaytooter.api.entity.TootAttachment import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.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.TootAttachment.Companion.tootAttachmentJson
import jp.juggler.subwaytooter.api.entity.TootAttachmentType 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.entity.parseItem
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.calcIconRound 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.dialog.showTextInputDialog
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.AttachmentRequest import jp.juggler.subwaytooter.util.AttachmentRequest
import jp.juggler.subwaytooter.util.AttachmentUploader
import jp.juggler.subwaytooter.util.PostAttachment import jp.juggler.subwaytooter.util.PostAttachment
import jp.juggler.subwaytooter.util.resolveMimeType
import jp.juggler.subwaytooter.view.MyNetworkImageView import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain 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.network.toPutRequestBuilder
import jp.juggler.util.ui.isLiveActivity import jp.juggler.util.ui.isLiveActivity
import jp.juggler.util.ui.vg import jp.juggler.util.ui.vg
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import java.nio.channels.ClosedChannelException
import kotlin.math.min import kotlin.math.min
private val log = LogCategory("ActPostAttachment") private val log = LogCategory("ActPostAttachment")
@ -132,140 +123,45 @@ fun ActPost.addAttachment(
uri: Uri, uri: Uri,
mimeTypeArg: String? = null, 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 account = this.account
if (account == null) {
val mimeType = uri.resolveMimeType(mimeTypeArg, this) dialogOrToast(R.string.account_select_please)
?.notEmpty() return
} else if (attachmentList.size >= 4) {
val isReply = states.inReplyToId != null dialogOrToast(R.string.attachment_too_many)
return
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
}
} }
val instance = getInstance() saveAttachmentList()
val pa = PostAttachment(this)
attachmentList.add(pa)
showMediaAttachment()
when { attachmentUploader.addRequest(
instance.instanceType == InstanceType.Pixelfed && isReply -> { AttachmentRequest(
AttachmentUploader.log.e("pixelfed_does_not_allow_reply_with_media") context = applicationContext,
dialogOrToast(R.string.pixelfed_does_not_allow_reply_with_media) account = account,
return pa = pa,
} uri = uri,
mimeTypeArg = mimeTypeArg,
else -> { isReply = states.inReplyToId != null,
saveAttachmentList() imageResizeConfig = account.getResizeConfig(),
val pa = PostAttachment(this) maxBytesVideo = { instance, mediaConfig ->
attachmentList.add(pa) min(
showMediaAttachment() account.getMovieMaxBytes(instance),
val mediaConfig = instance.configuration?.jsonObject("media_attachments") mediaConfig?.int("video_size_limit")
attachmentUploader.addRequest( ?.takeIf { it > 0 } ?: Int.MAX_VALUE,
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
) )
) },
} maxBytesImage = { instance, mediaConfig ->
} min(
} account.getImageMaxBytes(instance),
mediaConfig?.int("image_size_limit")
fun ActPost.launchAddAttachmentChannelReader() { ?.takeIf { it > 0 } ?: Int.MAX_VALUE,
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…")
}
}
}
}
} }
fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) { fun ActPost.onPostAttachmentCompleteImpl(pa: PostAttachment) {

View File

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

View File

@ -2,7 +2,6 @@ package jp.juggler.subwaytooter.api.auth
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import jp.juggler.subwaytooter.BuildConfig
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootParser 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.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.prefDevice import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.FcmFlavor
import jp.juggler.subwaytooter.table.daoClientInfo import jp.juggler.subwaytooter.table.daoClientInfo
import jp.juggler.subwaytooter.table.daoSavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.util.LinkHelper 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 import jp.juggler.util.log.LogCategory
class AuthMisskey10(override val client: TootApiClient) : AuthBase() { class AuthMisskey10(override val client: TootApiClient) : AuthBase() {
companion object { companion object {
private val log = LogCategory("MisskeyOldAuth") 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) = fun isCallbackUrl(uriStr: String) =
uriStr.startsWith(callbackUrl) || uriStr.startsWith(callbackUrl) ||
@ -213,7 +221,6 @@ class AuthMisskey10(override val client: TootApiClient) : AuthBase() {
linkHelper = LinkHelper.create(ti) linkHelper = LinkHelper.create(ti)
) )
@Suppress("UNUSED_VARIABLE")
val clientInfo = daoClientInfo.load(apiHost, clientName) val clientInfo = daoClientInfo.load(apiHost, clientName)
?.notEmpty() ?: error("missing client id") ?.notEmpty() ?: error("missing client id")

View File

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

View File

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

View File

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

View File

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

View File

@ -7,11 +7,14 @@ import android.os.Build
import jp.juggler.media.generateTempFile import jp.juggler.media.generateTempFile
import jp.juggler.media.transcodeAudio import jp.juggler.media.transcodeAudio
import jp.juggler.subwaytooter.R 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.api.entity.TootInstance
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.getStreamSize import jp.juggler.util.data.getStreamSize
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorEx import jp.juggler.util.log.errorEx
import jp.juggler.util.media.ResizeConfig import jp.juggler.util.media.ResizeConfig
@ -20,6 +23,7 @@ import jp.juggler.util.media.createResizedBitmap
import jp.juggler.util.media.transcodeVideo import jp.juggler.util.media.transcodeVideo
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.concurrent.CancellationException
import kotlin.math.min import kotlin.math.min
class AttachmentRequest( class AttachmentRequest(
@ -27,13 +31,11 @@ class AttachmentRequest(
val account: SavedAccount, val account: SavedAccount,
val pa: PostAttachment, val pa: PostAttachment,
val uri: Uri, val uri: Uri,
var mimeType: String, var mimeTypeArg: String?,
val imageResizeConfig: ResizeConfig, val imageResizeConfig: ResizeConfig,
val serverMaxSqPixel: Int?, val maxBytesVideo: (instance: TootInstance, mediaConfig: JsonObject?) -> Int,
val instance: TootInstance, val maxBytesImage: (instance: TootInstance, mediaConfig: JsonObject?) -> Int,
val mediaConfig: JsonObject?, val isReply: Boolean = false,
val maxBytesVideo: Int,
val maxBytesImage: Int,
) { ) {
companion object { companion object {
private val log = LogCategory("AttachmentRequest") private val log = LogCategory("AttachmentRequest")
@ -51,14 +53,49 @@ class AttachmentRequest(
"audio/x-wav", "audio/x-wav",
"audio/3gpp", "audio/3gpp",
) )
// val badAudioType = setOf(
// val badAudioType = setOf(
// "audio/mpeg","audio/aac", // "audio/mpeg","audio/aac",
// "audio/m4a","audio/x-m4a","audio/mp4", // "audio/m4a","audio/x-m4a","audio/mp4",
// "video/x-ms-asf", // "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 { suspend fun createOpener(): InputStreamOpener {
val mimeType = this.mimeType
// GIFはそのまま投げる // GIFはそのまま投げる
if (mimeType == MIME_TYPE_GIF) { if (mimeType == MIME_TYPE_GIF) {
@ -128,10 +165,12 @@ class AttachmentRequest(
) )
} }
private fun createResizedImageOpener(): InputStreamOpener { private suspend fun createResizedImageOpener(): InputStreamOpener {
try { try {
pa.progress = context.getString(R.string.attachment_handling_compress) pa.progress = context.getString(R.string.attachment_handling_compress)
val instance = instance()
val canUseWebP = try { val canUseWebP = try {
PrefB.bpUseWebP.value && MIME_TYPE_WEBP.mimeTypeIsSupported(instance) PrefB.bpUseWebP.value && MIME_TYPE_WEBP.mimeTypeIsSupported(instance)
} catch (ex: Throwable) { } catch (ex: Throwable) {
@ -154,7 +193,7 @@ class AttachmentRequest(
uri, uri,
imageResizeConfig, imageResizeConfig,
canSkip = canUseOriginal, canSkip = canUseOriginal,
serverMaxSqPixel = serverMaxSqPixel serverMaxSqPixel = serverMaxSqPixel()
)?.let { bitmap -> )?.let { bitmap ->
try { try {
return bitmap.compressAutoType(canUseWebP) return bitmap.compressAutoType(canUseWebP)
@ -273,14 +312,16 @@ class AttachmentRequest(
val tempFile = File(cacheDir, "movie." + Thread.currentThread().id + ".tmp") val tempFile = File(cacheDir, "movie." + Thread.currentThread().id + ".tmp")
val outFile = File(cacheDir, "movie." + Thread.currentThread().id + ".mp4") val outFile = File(cacheDir, "movie." + Thread.currentThread().id + ".mp4")
var resultFile: File? = null var resultFile: File? = null
try { try {
// 入力ファイルをコピーする // 入力ファイルをコピーする
(context.contentResolver.openInputStream(uri) (context.contentResolver.openInputStream(uri)
?: error("openInputStream returns null.")).use { inStream -> ?: error("openInputStream returns null.")).use { inStream ->
FileOutputStream(tempFile).use { inStream.copyTo(it) } FileOutputStream(tempFile).use { inStream.copyTo(it) }
} }
val mediaConfig = mediaConfig()
// 動画のメタデータを調べる // 動画のメタデータを調べる
val info = tempFile.videoInfo val info = tempFile.videoInfo
@ -330,11 +371,13 @@ class AttachmentRequest(
} }
} }
private suspend fun createResizedAudioOpener(srcBytes: Long): InputStreamOpener = private suspend fun createResizedAudioOpener(srcBytes: Long): InputStreamOpener {
when { val instance = instance()
val mediaConfig = mediaConfig()
return when {
mimeType.mimeTypeIsSupported(instance) && mimeType.mimeTypeIsSupported(instance) &&
goodAudioType.contains(mimeType) && goodAudioType.contains(mimeType) &&
srcBytes <= maxBytesVideo -> contentUriOpener( srcBytes <= maxBytesVideo(instance,mediaConfig).toLong() -> contentUriOpener(
context.contentResolver, context.contentResolver,
uri, uri,
mimeType, 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.TootApiResult
import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.EntityId 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.ServiceType
import jp.juggler.subwaytooter.api.entity.TootAttachment import jp.juggler.subwaytooter.api.entity.TootAttachment
import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.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.toPostRequestBuilder
import jp.juggler.util.network.toPut import jp.juggler.util.network.toPut
import jp.juggler.util.network.toPutRequestBuilder import jp.juggler.util.network.toPutRequestBuilder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MultipartBody import okhttp3.MultipartBody
import java.util.concurrent.CancellationException import java.nio.channels.ClosedChannelException
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
class AttachmentUploader( class AttachmentUploader(
@ -55,130 +57,137 @@ class AttachmentUploader(
private val safeContext = activity.applicationContext!! private val safeContext = activity.applicationContext!!
private var lastAttachmentAdd = 0L private var lastAttachmentAdd = 0L
private var lastAttachmentComplete = 0L private var lastAttachmentComplete = 0L
private var channel: Channel<AttachmentRequest>? = null private val channel = Channel<AttachmentRequest>(capacity = Channel.UNLIMITED)
private fun prepareChannel(): Channel<AttachmentRequest> { init {
// double check before/after lock launchIO {
channel?.let { return it } while (true) {
synchronized(this) { try {
channel?.let { return it } val request = channel.receive()
return Channel<AttachmentRequest>(capacity = Channel.UNLIMITED) if (request.pa.isCancelled) continue
.also { withContext(AppDispatchers.MainImmediate) {
channel = it val pa = request.pa
launchIO { pa.status = try {
while (true) { withContext(pa.job + AppDispatchers.IO) {
val request = try { request.upload()
it.receive()
} catch (ex: Throwable) {
when (ex) {
is CancellationException, is ClosedReceiveChannelException -> break
else -> {
safeContext.showToast(ex)
continue
}
}
} }
val result = try { request.pa.progress = ""
if (request.pa.isCancelled) continue
withContext(request.pa.job + AppDispatchers.IO) { val now = System.currentTimeMillis()
request.upload() if (now - lastAttachmentComplete >= 5000L) {
} safeContext.showToast(false, R.string.attachment_uploaded)
} catch (ex: Throwable) {
TootApiResult(ex.withCaption("upload failed."))
} }
try { lastAttachmentComplete = now
request.pa.progress = ""
withContext(AppDispatchers.MainImmediate) { PostAttachment.Status.Ok
handleResult(request, result) } catch (ex: Throwable) {
} if (ex is CancellationException) {
} catch (ex: Throwable) { // キャンセルはメッセージを出さない
when (ex) { } else if (ex.message?.contains("cancel", ignoreCase = true) == true) {
is CancellationException, is ClosedReceiveChannelException -> break // キャンセルはメッセージを出さない
else -> { } else if (ex is IllegalStateException) {
safeContext.showToast(ex) safeContext.showToast(true, "${ex.message}")
continue } 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() { fun onActivityDestroy() {
try { try {
synchronized(this) { channel.close()
channel?.close() } catch (ignored: Throwable) {
channel = null
}
} catch (ex: Throwable) {
log.e(ex, "can't close channel.")
} }
} }
fun addRequest(request: AttachmentRequest) { fun addRequest(request: AttachmentRequest) {
request.pa.progress = safeContext.getString(R.string.attachment_handling_start) 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順に表示するため // マストドンは添付メディアをID順に表示するため
// 画像が複数ある場合は一つずつ処理する必要がある // 画像が複数ある場合は一つずつ処理する必要がある
// 投稿画面ごとに1スレッドだけ作成してバックグラウンド処理を行う // 投稿画面ごとに作成したチャネルにsendして、受け側は順次処理する
launchIO { prepareChannel().send(request) } 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 @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 { 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) { val maxBytes = when (opener.isImage) {
true -> maxBytesImage true -> maxBytesImage(instance, mediaConfig)
else -> maxBytesVideo else -> maxBytesVideo(instance, mediaConfig)
} }
if (opener.contentLength > maxBytes) { if (opener.contentLength > maxBytes.toLong()) {
return TootApiResult( error(safeContext.getString(R.string.file_size_too_big, maxBytes / 1_000_000))
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))
}
if (!opener.mimeType.mimeTypeIsSupported(instance)) {
return TootApiResult(
safeContext.getString(R.string.mime_type_not_acceptable, opener.mimeType)
)
} }
val fileName = fixDocumentName(getDocumentName(safeContext.contentResolver, uri)) val fileName = fixDocumentName(getDocumentName(safeContext.contentResolver, uri))
pa.progress = safeContext.getString(R.string.attachment_handling_uploading, 0) pa.progress = safeContext.getString(R.string.attachment_handling_uploading, 0)
fun writeProgress(percent: Int) { fun writeProgress(percent: Int) {
if (percent < 100) { pa.progress = if (percent < 100) {
pa.progress = safeContext.getString(R.string.attachment_handling_uploading, percent)
safeContext.getString(R.string.attachment_handling_uploading, percent)
} else { } 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() val multipartBuilder = MultipartBody.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
@ -199,16 +208,13 @@ class AttachmentUploader(
) )
opener.deleteTempFile() opener.deleteTempFile()
val jsonObject = result?.jsonObject result ?: throw CancellationException()
if (jsonObject != null) {
val a = parseItem(jsonObject) { tootAttachment(ServiceType.MISSKEY, it) } val jsonObject = result.jsonObject
if (a == null) { ?: error(result.error ?: "missing error detail")
result.error = "TootAttachment.parse failed" pa.attachment =
} else { parseItem(jsonObject) { tootAttachment(ServiceType.MISSKEY, it) }
pa.attachment = a ?: error("TootAttachment.parse failed")
}
}
result
} else { } else {
suspend fun postMedia(path: String) = client.request( suspend fun postMedia(path: String) = client.request(
path, path,
@ -226,13 +232,14 @@ class AttachmentUploader(
suspend fun postV2(): TootApiResult? { suspend fun postV2(): TootApiResult? {
// 3.1.3未満は v1 APIを使う // 3.1.3未満は v1 APIを使う
if (!ti.versionGE(TootInstance.VERSION_3_1_3)) { if (!instance.versionGE(TootInstance.VERSION_3_1_3)) {
return postV1() return postV1()
} }
// v2 APIを試す // v2 APIを試す
val result = postMedia("/api/v2/media") 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 { when {
// 404ならv1 APIにフォールバック // 404ならv1 APIにフォールバック
code == 404 -> return postV1() code == 404 -> return postV1()
@ -242,18 +249,17 @@ class AttachmentUploader(
// ポーリングして処理完了を待つ // ポーリングして処理完了を待つ
pa.progress = safeContext.getString(R.string.attachment_handling_waiting_async) pa.progress = safeContext.getString(R.string.attachment_handling_waiting_async)
val id = parseItem(result?.jsonObject) {
val id = parseItem(result.jsonObject) {
tootAttachment(ServiceType.MASTODON, it) tootAttachment(ServiceType.MASTODON, it)
}?.id }?.id ?: error("/api/v2/media did not return the media ID.")
?: return TootApiResult("/api/v2/media did not return the media ID.")
var lastResponse = SystemClock.elapsedRealtime() var lastResponse = SystemClock.elapsedRealtime()
loop@ while (true) { while (true) {
delay(1000L) delay(1000L)
val r2 = client.request("/api/v1/media/$id") val r2 = client.request("/api/v1/media/$id")
?: return null // cancelled ?: throw CancellationException()
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
when (r2.response?.code) { when (r2.response?.code) {
@ -264,29 +270,22 @@ class AttachmentUploader(
206 -> lastResponse = now 206 -> lastResponse = now
// temporary errors, check timeout without 206 response. // temporary errors, check timeout without 206 response.
else -> if (now - lastResponse >= 120000L) { else -> if (now - lastResponse >= 120000L) error("timeout.")
return TootApiResult("timeout.")
}
} }
} }
} }
val result = postV2() val result = postV2()
opener.deleteTempFile() ?: throw CancellationException()
val jsonObject = result?.jsonObject val jsonObject = result.jsonObject
if (jsonObject != null) { ?: error(result.error ?: "missing error detail")
when (val a = parseItem(jsonObject) {
tootAttachment(ServiceType.MASTODON, it) pa.attachment = parseItem(jsonObject) { tootAttachment(ServiceType.MASTODON, it) }
}) { ?: error("TootAttachment.parse failed")
null -> result.error = "TootAttachment.parse failed"
else -> pa.attachment = a
}
}
result
} }
} catch (ex: Throwable) { } finally {
return TootApiResult(ex.withCaption("read failed.")) opener.deleteTempFile()
} }
} }
@ -306,38 +305,6 @@ class AttachmentUploader(
return sb.toString() 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( suspend fun uploadCustomThumbnail(
@ -346,67 +313,62 @@ class AttachmentUploader(
pa: PostAttachment, pa: PostAttachment,
): TootApiResult? = try { ): TootApiResult? = try {
safeContext.runApiTask(account) { client -> 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( val ar = AttachmentRequest(
context = safeContext, context = safeContext,
account = account, account = account,
pa = pa, pa = pa,
uri = src.uri, uri = src.uri,
mimeType = mimeType, mimeTypeArg = src.mimeType,
instance = ti,
mediaConfig = mediaConfig,
imageResizeConfig = ResizeConfig(ResizeType.SquarePixel, 400), imageResizeConfig = ResizeConfig(ResizeType.SquarePixel, 400),
serverMaxSqPixel = mediaConfig?.int("image_matrix_limit")?.takeIf { it > 0 }, maxBytesImage = { _, _ -> 1000000 },
maxBytesImage = 1000000, maxBytesVideo = { _, _ -> 1000000 },
maxBytesVideo = 1000000,
) )
val instance = ar.instance()
val mediaConfig = ar.mediaConfig()
val maxBytesImage = ar.maxBytesImage(instance, mediaConfig)
val opener = ar.createOpener() val opener = ar.createOpener()
if (opener.contentLength > ar.maxBytesImage) { try{
return@runApiTask TootApiResult(
getString(
R.string.file_size_too_big,
ar.maxBytesImage / 1000000
)
)
}
val fileName = fixDocumentName(getDocumentName(safeContext.contentResolver, src.uri)) if (opener.contentLength > maxBytesImage.toLong()) {
return@runApiTask TootApiResult(
if (account.isMisskey) { getString(
opener.deleteTempFile() R.string.file_size_too_big,
TootApiResult("custom thumbnail is not supported on misskey account.") maxBytesImage / 1000000
} else {
val result = client.request(
"/api/v1/media/${pa.attachment?.id}",
MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart(
"thumbnail",
fileName,
opener.toRequestBody(),
) )
.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) { } 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 android.os.Looper
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes 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.TransformationRequest
import androidx.media3.transformer.TransformationResult
import androidx.media3.transformer.Transformer import androidx.media3.transformer.Transformer
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -57,35 +59,39 @@ suspend fun transcodeAudio(
val tmpFile = context.generateTempFile("transcodeAudio") val tmpFile = context.generateTempFile("transcodeAudio")
// Transformerは単一スレッドで処理する要件 // Transformerは単一スレッドで処理する要件
val result: TransformationResult = withContext(Dispatchers.Main.immediate) { val result: ExportResult = withContext(Dispatchers.Main.immediate) {
val looper = Looper.getMainLooper() val looper = Looper.getMainLooper()
suspendCancellableCoroutine { cont -> suspendCancellableCoroutine { cont ->
val transformerListener = object : Transformer.Listener { val transformerListener = object : Transformer.Listener {
override fun onTransformationCompleted( override fun onCompleted(
inputMediaItem: MediaItem, composition: Composition,
transformationResult: TransformationResult, exportResult: ExportResult,
) { ) {
log.i("onTransformationCompleted inputMediaItem=$inputMediaItem transformationResult=$transformationResult") val mediaItem = composition.sequences[0].editedMediaItems[0].mediaItem
if (cont.isActive) cont.resume(transformationResult) {} log.i("onCompleted mediaItem=$mediaItem exportResult=$exportResult")
if (cont.isActive) cont.resume(exportResult) {}
} }
override fun onTransformationError( override fun onError(
inputMediaItem: MediaItem, composition: Composition,
exception: TransformationException, exportResult: ExportResult,
exportException: ExportException,
) { ) {
val mediaItem = composition.sequences[0].editedMediaItems[0].mediaItem
log.e( log.e(
exception, exportException,
"onTransformationError inputMediaItem=$inputMediaItem" "onError inputMediaItem=$mediaItem, exportResult=$exportResult"
) )
if (cont.isActive) cont.resumeWithException(exception) if (cont.isActive) cont.resumeWithException(exportException)
} }
override fun onFallbackApplied( override fun onFallbackApplied(
inputMediaItem: MediaItem, composition: Composition,
originalTransformationRequest: TransformationRequest, originalTransformationRequest: TransformationRequest,
fallbackTransformationRequest: 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) val transformer = Transformer.Builder(context)
@ -95,11 +101,13 @@ suspend fun transcodeAudio(
.setAudioMimeType(encodeMimeType) .setAudioMimeType(encodeMimeType)
.build() .build()
) )
.setRemoveVideo(true)
.setRemoveAudio(false)
.addListener(transformerListener) .addListener(transformerListener)
.build() .build()
transformer.startTransformation(inputMediaItem, tmpFile.canonicalPath)
val editedMediaItem = EditedMediaItem.Builder(inputMediaItem).apply {
setRemoveVideo(true)
}.build()
transformer.start(editedMediaItem, tmpFile.canonicalPath)
cont.invokeOnCancellation { cont.invokeOnCancellation {
transformer.cancel() transformer.cancel()
} }

View File

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