Compare commits

...

3 Commits

Author SHA1 Message Date
tateisu 77d780881c v5.543 2024-01-03 00:26:50 +09:00
tateisu 3c975f40ee fix #258, TTS読み上げ完了のブロードキャスト受信のAndroid 14対応漏れ 2024-01-03 00:25:58 +09:00
tateisu f4093abba9 コード整理 2024-01-03 00:18:09 +09:00
11 changed files with 124 additions and 85 deletions

View File

@ -4,10 +4,19 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="GRADLE" /> <compositeConfiguration>
<option name="distributionType" value="DEFAULT_WRAPPED" /> <compositeBuild compositeDefinitionSource="SCRIPT">
<builds>
<build path="$PROJECT_DIR$/buildSrc" name="buildSrc">
<projects>
<project path="$PROJECT_DIR$/buildSrc" />
</projects>
</build>
</builds>
</compositeBuild>
</compositeConfiguration>
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="zulu-17" /> <option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
@ -23,6 +32,7 @@
<option value="$PROJECT_DIR$/sample_apng" /> <option value="$PROJECT_DIR$/sample_apng" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View File

@ -25,8 +25,8 @@ android {
defaultConfig { defaultConfig {
targetSdk = Vers.stTargetSdkVersion targetSdk = Vers.stTargetSdkVersion
minSdk = Vers.stMinSdkVersion minSdk = Vers.stMinSdkVersion
versionCode = 543 versionCode = 544
versionName = "5.543" versionName = "5.544"
applicationId = "jp.juggler.subwaytooter" applicationId = "jp.juggler.subwaytooter"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@ -87,6 +87,7 @@
<application <application
android:name=".App1" android:name=".App1"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_spec" android:fullBackupContent="@xml/backup_spec"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"

View File

@ -20,8 +20,8 @@ class ActCallback : AppCompatActivity() {
companion object { companion object {
private val log = LogCategory("ActCallback") private val log = LogCategory("ActCallback")
internal val last_uri = AtomicReference<Uri>(null) internal val lastUri = AtomicReference<Uri>(null)
internal val sent_intent = AtomicReference<Intent>(null) internal val sharedIntent = AtomicReference<Intent>(null)
private fun String?.isMediaMimeType() = when { private fun String?.isMediaMimeType() = when {
this == null -> false this == null -> false
@ -47,10 +47,10 @@ class ActCallback : AppCompatActivity() {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
log.d("onCreate flags=0x${intent.flags.toString(radix = 16)}") var intent = this.intent
log.d("onCreate flags=0x${intent?.flags?.toString(radix = 16)}")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
var intent: Intent? = intent
when { when {
intent == null -> { intent == null -> {
// 多分起きないと思う // 多分起きないと思う
@ -65,29 +65,31 @@ class ActCallback : AppCompatActivity() {
val type = intent.type val type = intent.type
// ACTION_SEND か ACTION_SEND_MULTIPLE // ACTION_SEND か ACTION_SEND_MULTIPLE
// ACTION_VIEW かつ type が 画像かビデオか音声 // ACTION_VIEW かつ type が 画像かビデオか音声
if ( when {
Intent.ACTION_SEND == action || Intent.ACTION_SEND == action ||
Intent.ACTION_SEND_MULTIPLE == action || Intent.ACTION_SEND_MULTIPLE == action ||
(Intent.ACTION_VIEW == action && type.isMediaMimeType()) (Intent.ACTION_VIEW == action && type.isMediaMimeType()) -> {
) {
// Google Photo などから送られるIntentに含まれるuriの有効期間はActivityが閉じられるまで // Google Photo などから送られるIntentに含まれるuriの有効期間はActivityが閉じられるまで
// http://qiita.com/pside/items/a821e2fe9ae6b7c1a98c // http://qiita.com/pside/items/a821e2fe9ae6b7c1a98c
// 有効期間を延長する // 有効期間を延長する
intent = remake(intent) intent = remake(intent)
if (intent != null) { if (intent != null) {
sent_intent.set(intent) sharedIntent.set(intent)
}
} }
} else if (forbidUriFromApp(intent)) { forbidUriFromApp(intent) -> {
// last_uriをクリアする // last_uriをクリアする
last_uri.set(null) lastUri.set(null)
// ダイアログを閉じるまで画面遷移しない // ダイアログを閉じるまで画面遷移しない
return return
} else { }
val uri = intent.data else -> {
if (uri != null) { val uri = intent.data
last_uri.set(uri) if (uri != null) {
lastUri.set(uri)
}
} }
} }
} }
@ -105,13 +107,11 @@ class ActCallback : AppCompatActivity() {
} }
private fun copyExtraTexts(dst: Intent, src: Intent) { private fun copyExtraTexts(dst: Intent, src: Intent) {
var sv: String? src.string(Intent.EXTRA_TEXT)
?.let { dst.putExtra(Intent.EXTRA_TEXT, it) }
// //
sv = src.string(Intent.EXTRA_TEXT) src.string(Intent.EXTRA_SUBJECT)
if (sv != null) dst.putExtra(Intent.EXTRA_TEXT, sv) ?.let { dst.putExtra(Intent.EXTRA_SUBJECT, it) }
//
sv = src.string(Intent.EXTRA_SUBJECT)
if (sv != null) dst.putExtra(Intent.EXTRA_SUBJECT, sv)
} }
private fun remake(src: Intent): Intent? { private fun remake(src: Intent): Intent? {
@ -123,52 +123,56 @@ class ActCallback : AppCompatActivity() {
val type = src.type val type = src.type
if (type.isMediaMimeType()) { if (type.isMediaMimeType()) {
if (Intent.ACTION_VIEW == action) { when (action){
src.data?.let { uriOriginal -> Intent.ACTION_VIEW -> {
src.data?.let { uriOriginal ->
try {
val uri = saveToCache(uriOriginal)
val dst = Intent(action)
dst.setDataAndType(uri, type)
copyExtraTexts(dst, src)
return dst
} catch (ex: Throwable) {
log.e(ex, "remake failed. src=$src")
}
}
}
Intent.ACTION_SEND -> {
var uri = src.getStreamUriExtra()
?: return src // text/plainの場合
try { try {
val uri = saveToCache(uriOriginal) uri = saveToCache(uri)
val dst = Intent(action) val dst = Intent(action)
dst.setDataAndType(uri, type) dst.type = type
dst.putExtra(Intent.EXTRA_STREAM, uri)
copyExtraTexts(dst, src) copyExtraTexts(dst, src)
return dst return dst
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "remake failed. src=$src") log.e(ex, "remake failed. src=$src")
} }
} }
} else if (Intent.ACTION_SEND == action) { Intent.ACTION_SEND_MULTIPLE -> {
var uri = src.getStreamUriExtra() val listUri = src.getStreamUriListExtra()
?: return src // text/plainの場合 ?: return null
try { val listDst = ArrayList<Uri>()
uri = saveToCache(uri) for (uriOriginal in listUri) {
if (uriOriginal != null) {
val dst = Intent(action) try {
dst.type = type val uri = saveToCache(uriOriginal)
dst.putExtra(Intent.EXTRA_STREAM, uri) listDst.add(uri)
copyExtraTexts(dst, src) } catch (ex: Throwable) {
return dst log.e(ex, "remake failed. src=$src")
} catch (ex: Throwable) { }
log.e(ex, "remake failed. src=$src")
}
} else if (Intent.ACTION_SEND_MULTIPLE == action) {
val listUri = src.getStreamUriListExtra()
?: return null
val listDst = ArrayList<Uri>()
for (uriOriginal in listUri) {
if (uriOriginal != null) {
try {
val uri = saveToCache(uriOriginal)
listDst.add(uri)
} catch (ex: Throwable) {
log.e(ex, "remake failed. src=$src")
} }
} }
if (listDst.isEmpty()) return null
val dst = Intent(action)
dst.type = type
dst.putParcelableArrayListExtra(Intent.EXTRA_STREAM, listDst)
copyExtraTexts(dst, src)
return dst
} }
if (listDst.isEmpty()) return null
val dst = Intent(action)
dst.type = type
dst.putParcelableArrayListExtra(Intent.EXTRA_STREAM, listDst)
copyExtraTexts(dst, src)
return dst
} }
} else if (Intent.ACTION_SEND == action) { } else if (Intent.ACTION_SEND == action) {

View File

@ -657,13 +657,13 @@ class ActMain : AppCompatActivity(),
} }
// 外部から受け取ったUriの処理 // 外部から受け取ったUriの処理
val uri = ActCallback.last_uri.getAndSet(null) val uri = ActCallback.lastUri.getAndSet(null)
if (uri != null) { if (uri != null) {
handleIntentUri(uri) handleIntentUri(uri)
} }
// 外部から受け取ったUriの処理 // 外部から受け取ったUriの処理
val intent = ActCallback.sent_intent.getAndSet(null) val intent = ActCallback.sharedIntent.getAndSet(null)
if (intent != null) { if (intent != null) {
handleSharedIntent(intent) handleSharedIntent(intent)
} }

View File

@ -13,6 +13,7 @@ import android.os.Environment
import android.os.SystemClock import android.os.SystemClock
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -20,6 +21,7 @@ import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Timeline import androidx.media3.common.Timeline
import androidx.media3.common.util.RepeatModeUtil import androidx.media3.common.util.RepeatModeUtil
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.LoadEventInfo import androidx.media3.exoplayer.source.LoadEventInfo
import androidx.media3.exoplayer.source.MediaLoadData import androidx.media3.exoplayer.source.MediaLoadData
@ -60,7 +62,6 @@ import javax.net.ssl.HttpsURLConnection
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@androidx.annotation.OptIn(markerClass = [androidx.media3.common.util.UnstableApi::class])
class ActMediaViewer : AppCompatActivity(), View.OnClickListener { class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
companion object { companion object {
@ -226,6 +227,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
} }
} }
@UnstableApi
private val mediaSourceEventListener = object : MediaSourceEventListener { private val mediaSourceEventListener = object : MediaSourceEventListener {
override fun onLoadStarted( override fun onLoadStarted(
windowIndex: Int, windowIndex: Int,
@ -306,7 +308,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
this.showDescription = intent.getBooleanExtra(EXTRA_SHOW_DESCRIPTION, showDescription) this.showDescription = intent.getBooleanExtra(EXTRA_SHOW_DESCRIPTION, showDescription)
this.serviceType = ServiceType.values()[ this.serviceType = ServiceType.entries[
savedInstanceState?.int(EXTRA_SERVICE_TYPE) savedInstanceState?.int(EXTRA_SERVICE_TYPE)
?: intent.int(EXTRA_SERVICE_TYPE) ?: 0 ?: intent.int(EXTRA_SERVICE_TYPE) ?: 0
] ]
@ -349,7 +351,8 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
) )
} }
internal fun initUI() { @OptIn(UnstableApi::class)
private fun initUI() {
setContentView(views.root) setContentView(views.root)
views.pbvImage.background = MediaBackgroundDrawable( views.pbvImage.background = MediaBackgroundDrawable(
@ -409,6 +412,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
exoPlayer = ExoPlayer.Builder(this).build() exoPlayer = ExoPlayer.Builder(this).build()
exoPlayer.addListener(playerListener) exoPlayer.addListener(playerListener)
views.pvVideo.run { views.pvVideo.run {
player = exoPlayer player = exoPlayer
controllerAutoShow = false controllerAutoShow = false
@ -916,7 +920,7 @@ class ActMediaViewer : AppCompatActivity(), View.OnClickListener {
private fun mediaBackgroundDialog() { private fun mediaBackgroundDialog() {
launchAndShowError { launchAndShowError {
actionsDialog(getString(R.string.background_pattern)) { actionsDialog(getString(R.string.background_pattern)) {
for (k in MediaBackgroundDrawable.Kind.values()) { for (k in MediaBackgroundDrawable.Kind.entries) {
if (!k.isMediaBackground) continue if (!k.isMediaBackground) continue
action(k.name) { action(k.name) {
val idx = k.toIndex() val idx = k.toIndex()

View File

@ -10,6 +10,7 @@ import android.os.SystemClock
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
import android.speech.tts.Voice import android.speech.tts.Voice
import android.text.Spannable import android.text.Spannable
import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.ColumnEncoder import jp.juggler.subwaytooter.column.ColumnEncoder
@ -190,9 +191,9 @@ class AppState(
private val ttsReceiver = object : BroadcastReceiver() { private val ttsReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
if (intent != null) { log.i("ttsReceiver onReceive action=${intent?.action}")
if (TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED == intent.action) { when (intent?.action) {
log.d("tts_receiver: speech completed.") TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED -> {
ttsSpeakEnd = SystemClock.elapsedRealtime() ttsSpeakEnd = SystemClock.elapsedRealtime()
handler.post(procFlushSpeechQueue) handler.post(procFlushSpeechQueue)
} }
@ -411,7 +412,7 @@ class AppState(
} catch (ignored: Throwable) { } catch (ignored: Throwable) {
null null
} }
if (voiceSet == null || voiceSet.isEmpty()) { if (voiceSet.isNullOrEmpty()) {
log.d("TextToSpeech.getVoices returns null or empty set.") log.d("TextToSpeech.getVoices returns null or empty set.")
} else { } else {
val lang = defaultLocale(context).toLanguageTag() val lang = defaultLocale(context).toLanguageTag()
@ -427,9 +428,13 @@ class AppState(
handler.post(procFlushSpeechQueue) handler.post(procFlushSpeechQueue)
context.registerReceiver( ContextCompat.registerReceiver(
context,
ttsReceiver, ttsReceiver,
IntentFilter(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED) IntentFilter(TextToSpeech.ACTION_TTS_QUEUE_PROCESSING_COMPLETED),
ContextCompat.RECEIVER_EXPORTED,
// RECEIVER_NOT_EXPORTED だと読み上げ完了を受け取れない
// ContextCompat.RECEIVER_NOT_EXPORTED,
) )
// tts.setOnUtteranceProgressListener( new UtteranceProgressListener() { // tts.setOnUtteranceProgressListener( new UtteranceProgressListener() {

View File

@ -21,7 +21,7 @@ import jp.juggler.subwaytooter.calcIconRound
import jp.juggler.subwaytooter.defaultColorIcon import jp.juggler.subwaytooter.defaultColorIcon
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.decodeAttachmentBitmap import jp.juggler.subwaytooter.dialog.decodeAttachmentBitmap
import jp.juggler.subwaytooter.dialog.dialogArrachmentRearrange import jp.juggler.subwaytooter.dialog.dialogAttachmentRearrange
import jp.juggler.subwaytooter.dialog.focusPointDialog 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
@ -454,7 +454,7 @@ fun ActPost.onPickCustomThumbnailImpl(pa: PostAttachment, src: GetContentResultE
fun ActPost.rearrangeAttachments() = lifecycleScope.launch { fun ActPost.rearrangeAttachments() = lifecycleScope.launch {
try { try {
val rearranged = dialogArrachmentRearrange(attachmentList) val rearranged = dialogAttachmentRearrange(attachmentList)
// 入れ替え中にアップロード失敗などで要素が消えることがあるので // 入れ替え中にアップロード失敗などで要素が消えることがあるので
// 最新のattachmentListを指定順に並べ替える // 最新のattachmentListを指定順に並べ替える
val remain = ArrayList(attachmentList) val remain = ArrayList(attachmentList)

View File

@ -31,7 +31,7 @@ private val log = LogCategory("DlgAttachmentRearrange")
* 投稿画面で添付メディアを並べ替えるダイアログを開きOKボタンが押されるまで非同期待機する * 投稿画面で添付メディアを並べ替えるダイアログを開きOKボタンが押されるまで非同期待機する
* OK以外の方法で閉じたらCancellationExceptionを投げる * OK以外の方法で閉じたらCancellationExceptionを投げる
*/ */
suspend fun AppCompatActivity.dialogArrachmentRearrange( suspend fun AppCompatActivity.dialogAttachmentRearrange(
initialList: List<PostAttachment>, initialList: List<PostAttachment>,
): List<PostAttachment> = suspendCancellableCoroutine { cont -> ): List<PostAttachment> = suspendCancellableCoroutine { cont ->
val views = AttachmentRearrangeDialogBinding.inflate(layoutInflater) val views = AttachmentRearrangeDialogBinding.inflate(layoutInflater)

View File

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<full-backup-content> <full-backup-content>
<exclude domain="sharedpref" path="device.xml"/> <exclude
domain="sharedpref"
path="device.xml" />
</full-backup-content> </full-backup-content>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude
domain="sharedpref"
path="device.xml" />
</cloud-backup>
<device-transfer>
<exclude
domain="sharedpref"
path="device.xml" />
</device-transfer>
</data-extraction-rules>