diff --git a/apng_android/build.gradle b/apng_android/build.gradle index dab34967..be7e330c 100644 --- a/apng_android/build.gradle +++ b/apng_android/build.gradle @@ -46,6 +46,5 @@ dependencies { api project(":apng") implementation project(":base") - implementation "com.github.zjupure:webpdecoder:2.3.$glideVersion" testImplementation "junit:junit:$junitVersion" } diff --git a/apng_android/proguard-rules.pro b/apng_android/proguard-rules.pro index 73657cf8..8bd2df79 100644 --- a/apng_android/proguard-rules.pro +++ b/apng_android/proguard-rules.pro @@ -21,3 +21,7 @@ #-renamesourcefileattribute SourceFile -dontobfuscate + +-keep public class com.bumptech.glide.integration.webp.WebpImage { *; } +-keep public class com.bumptech.glide.integration.webp.WebpFrame { *; } +-keep public class com.bumptech.glide.integration.webp.WebpBitmapFactory { *; } diff --git a/app/build.gradle b/app/build.gradle index e300c2b3..e2361152 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,6 +68,12 @@ android { lintOptions { disable "MissingTranslation" } + // You need to specify either an absolute path or include the + // keystore file in the same directory as the build.gradle file. + storeFile file("D:\\GoogleDrive\\_private\\AndroidSignKeys\\SubwayTooter.jks") + storePassword "password" + keyAlias "my-alias" + keyPassword "password" } debug { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index afae16cc..610861c3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -60,6 +60,10 @@ kotlinx.serialization.KSerializer serializer(...); } +-keep public class com.bumptech.glide.integration.webp.WebpImage { *; } +-keep public class com.bumptech.glide.integration.webp.WebpFrame { *; } +-keep public class com.bumptech.glide.integration.webp.WebpBitmapFactory { *; } + # keep everything -keep class ** { *; } -keepclassmembers class ** { *** Companion; } diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt index 9eed9098..a17f4fa0 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/TestDuplicateMap.kt @@ -1,8 +1,12 @@ package jp.juggler.subwaytooter.api -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef +import jp.juggler.subwaytooter.api.entity.TootNotification.Companion.tootNotification +import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.data.* import org.junit.Assert.assertEquals @@ -36,7 +40,7 @@ class TestDuplicateMap { if (url != null) put("url", url) } - return TootStatus(parser, itemJson) + return tootStatus(parser, itemJson) } private fun testDuplicateStatus(): ArrayList { @@ -63,7 +67,7 @@ class TestDuplicateMap { put("url", "http://${parser.apiHost}/@user1") } - val account1 = TootAccount(parser, account1Json) + val account1 = tootAccount(parser, account1Json) assertNotNull(account1) val map = DuplicateMap() @@ -122,7 +126,7 @@ class TestDuplicateMap { put("id", id) } - val item = TootNotification(parser, itemJson) + val item = tootNotification(parser, itemJson) assertNotNull(item) generatedItems.add(item) assertEquals(false, map.isDuplicate(item)) @@ -178,7 +182,7 @@ class TestDuplicateMap { put("url", "http://${parser.apiHost}/@user$id") } - val item = TootAccountRef.notNull(parser, TootAccount(parser, itemJson)) + val item = tootAccountRef(parser, tootAccount(parser, itemJson)) assertNotNull(item) generatedItems.add(item) assertEquals(false, map.isDuplicate(item)) diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt index 9e07ac38..e0ffd9b0 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/api/entity/TestEntityUtils.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter.api.entity -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.data.* @@ -30,42 +30,42 @@ class TestEntityUtils { @Test fun testParseItem() { - assertEquals(null, parseItem(::TestEntity, null)) + assertEquals(null, parseItem(null, ::TestEntity)) run { val src = """{"s":null,"l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) + val item = parseItem(src, ::TestEntity) assertNull(item) } run { val src = """{"s":"","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) + val item = parseItem(src, ::TestEntity) assertNull(item) } run { val src = """{"s":"A","l":null}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) + val item = parseItem(src, ::TestEntity) assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) } run { val src = """{"s":"A","l":""}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) + val item = parseItem(src, ::TestEntity) assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) } run { val src = """{"s":"A","l":100}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) + val item = parseItem(src, ::TestEntity) assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) } run { val src = """{"s":"A","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, src) + val item = parseItem(src, ::TestEntity) assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) @@ -74,74 +74,74 @@ class TestEntityUtils { @Test fun testParseList() { - assertEquals(0, parseList(::TestEntity, null).size) + assertEquals(0, parseList(null, ::TestEntity).size) val src = JsonArray() - assertEquals(0, parseList(::TestEntity, src).size) + assertEquals(0, parseList(src, ::TestEntity).size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseList(::TestEntity, src).size) + assertEquals(1, parseList(src, ::TestEntity).size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, src).size) + assertEquals(2, parseList(src, ::TestEntity).size) // error src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, src).size) + assertEquals(2, parseList(src, ::TestEntity).size) } @Test fun testParseListOrNull() { - assertEquals(null, parseListOrNull(::TestEntity, null)) + assertEquals(null, parseListOrNull(null, ::TestEntity)) val src = JsonArray() - assertEquals(null, parseListOrNull(::TestEntity, src)) + assertEquals(null, parseListOrNull(src, ::TestEntity)) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseListOrNull(::TestEntity, src)?.size) + assertEquals(1, parseListOrNull(src, ::TestEntity)?.size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, src)?.size) + assertEquals(2, parseListOrNull(src, ::TestEntity)?.size) // error src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, src)?.size) + assertEquals(2, parseListOrNull(src, ::TestEntity)?.size) } @Test fun testParseMap() { - assertEquals(0, parseMap(::TestEntity, null).size) + assertEquals(0, parseMap(null, ::TestEntity).size) val src = JsonArray() - assertEquals(0, parseMap(::TestEntity, src).size) + assertEquals(0, parseMap(null, ::TestEntity).size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseMap(::TestEntity, src).size) + assertEquals(1, parseMap(src, ::TestEntity).size) src.add("""{"s":"B","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMap(::TestEntity, src).size) + assertEquals(2, parseMap(src, ::TestEntity).size) // error src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMap(::TestEntity, src).size) + assertEquals(2, parseMap(src, ::TestEntity).size) } @Test fun testParseMapOrNull() { - assertEquals(null, parseMapOrNull(::TestEntity, null)) + assertEquals(null, parseMapOrNull(null, ::TestEntity)) val src = JsonArray() - assertEquals(null, parseMapOrNull(::TestEntity, src)) + assertEquals(null, parseMapOrNull(src, ::TestEntity)) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseMapOrNull(::TestEntity, src)?.size) + assertEquals(1, parseMapOrNull(src, ::TestEntity)?.size) src.add("""{"s":"B","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMapOrNull(::TestEntity, src)?.size) + assertEquals(2, parseMapOrNull(src, ::TestEntity)?.size) // error src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseMapOrNull(::TestEntity, src)?.size) + assertEquals(2, parseMapOrNull(src, ::TestEntity)?.size) } private val parser by lazy { @@ -154,42 +154,42 @@ class TestEntityUtils { @Test fun testParseItemWithParser() { - assertEquals(null, parseItem(::TestEntity, parser, null)) + assertEquals(null, parseItem(null) { TestEntity(parser, it) }) run { val src = """{"s":null,"l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) + val item = parseItem(src) { TestEntity(parser, it) } assertNull(item) } run { val src = """{"s":"","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) + val item = parseItem(src) { TestEntity(parser, it) } assertNull(item) } run { val src = """{"s":"A","l":null}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) + val item = parseItem(src) { TestEntity(parser, it) } assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) } run { val src = """{"s":"A","l":""}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) + val item = parseItem(src) { TestEntity(parser, it) } assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) } run { val src = """{"s":"A","l":100}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) + val item = parseItem(src) { TestEntity(parser, it) } assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) } run { val src = """{"s":"A","l":"100"}""".decodeJsonObject() - val item = parseItem(::TestEntity, parser, src) + val item = parseItem(src) { TestEntity(parser, it) } assertNotNull(item) assertEquals(src.optString("s"), item?.s) assertEquals(src.optLong("l"), item?.l) @@ -198,38 +198,38 @@ class TestEntityUtils { @Test fun testParseListWithParser() { - assertEquals(0, parseList(::TestEntity, parser, null).size) + assertEquals(0, parseList(null) { TestEntity(parser, it) }.size) val src = JsonArray() - assertEquals(0, parseList(::TestEntity, parser, src).size) + assertEquals(0, parseList(src) { TestEntity(parser, it) }.size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseList(::TestEntity, parser, src).size) + assertEquals(1, parseList(src) { TestEntity(parser, it) }.size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, parser, src).size) + assertEquals(2, parseList(src) { TestEntity(parser, it) }.size) // error src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseList(::TestEntity, parser, src).size) + assertEquals(2, parseList(src) { TestEntity(parser, it) }.size) } @Test fun testParseListOrNullWithParser() { - assertEquals(null, parseListOrNull(::TestEntity, parser, null)) + assertEquals(null, parseListOrNull(null) { TestEntity(parser, it) }) val src = JsonArray() - assertEquals(null, parseListOrNull(::TestEntity, parser, src)) + assertEquals(null, parseListOrNull(src) { TestEntity(parser, it) }) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(1, parseListOrNull(::TestEntity, parser, src)?.size) + assertEquals(1, parseListOrNull(src) { TestEntity(parser, it) }?.size) src.add("""{"s":"A","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size) + assertEquals(2, parseListOrNull(src) { TestEntity(parser, it) }?.size) // error src.add("""{"s":"","l":"100"}""".decodeJsonObject()) - assertEquals(2, parseListOrNull(::TestEntity, parser, src)?.size) + assertEquals(2, parseListOrNull(src) { TestEntity(parser, it) }?.size) } @Test(expected = RuntimeException::class) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt index 4716dc12..39e6d77a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAccountSetting.kt @@ -5,6 +5,7 @@ import android.content.ContentValues import android.content.Context import android.content.Intent import android.graphics.Bitmap +import android.graphics.Color import android.net.Uri import android.os.Bundle import android.os.Handler @@ -16,12 +17,16 @@ import android.view.View import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.jrummyapps.android.colorpicker.ColorPickerDialog +import com.jrummyapps.android.colorpicker.ColorPickerDialogListener import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.auth.authRepo import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment import jp.juggler.subwaytooter.databinding.ActAccountSettingBinding +import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.notification.* import jp.juggler.subwaytooter.push.PushBase @@ -64,6 +69,7 @@ import kotlin.math.max class ActAccountSetting : AppCompatActivity(), View.OnClickListener, + ColorPickerDialogListener, CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener { companion object { @@ -84,6 +90,8 @@ class ActAccountSetting : AppCompatActivity(), private const val ACTIVITY_STATE = "MyActivityState" + private const val COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR = 1 + fun createIntent(activity: Activity, ai: SavedAccount) = Intent(activity, ActAccountSetting::class.java).apply { putExtra(KEY_ACCOUNT_DB_ID, ai.db_id) @@ -510,6 +518,8 @@ class ActAccountSetting : AppCompatActivity(), spResizeImage, swNotificationPullEnabled, swNotificationPushEnabled, + btnNotificationAccentColorEdit, + btnNotificationAccentColorReset, ).forEach { it.isEnabledAlpha = enabled } // arrayOf( @@ -521,6 +531,7 @@ class ActAccountSetting : AppCompatActivity(), showVisibility() showAcctColor() showPushSetting() + showNotificationColor() } finally { loadingBusy = false } @@ -547,6 +558,8 @@ class ActAccountSetting : AppCompatActivity(), tvPushActions.vg(usePush) btnPushSubscription.vg(usePush) btnPushSubscriptionNotForce.vg(usePush) + tvNotificationAccentColor.vg(usePush) + llNotificationAccentColor.vg(usePush) } run { @@ -688,6 +701,22 @@ class ActAccountSetting : AppCompatActivity(), // PullNotification.openNotificationChannelSetting( // this // ) + + R.id.btnNotificationAccentColorEdit -> { + ColorPickerDialog.newBuilder().apply { + setDialogType(ColorPickerDialog.TYPE_CUSTOM) + setAllowPresets(true) + setShowAlphaSlider(false) + setDialogId(COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR) + account.notificationAccentColor.notZero()?.let { setColor(it) } + }.show(this) + } + + R.id.btnNotificationAccentColorReset -> { + account.notificationAccentColor = 0 + saveUIToData() + showNotificationColor() + } } } @@ -788,16 +817,11 @@ class ActAccountSetting : AppCompatActivity(), /////////////////////////////////////////////////// private fun performAccountRemove() { - AlertDialog.Builder(this) - .setTitle(R.string.confirm) - .setMessage(R.string.confirm_account_remove) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok) { _, _ -> - launchAndShowError { - authRepo.accountRemove(account) - finish() - } - }.show() + launchAndShowError { + confirm(getString(R.string.confirm_account_remove), title = getString(R.string.confirm)) + authRepo.accountRemove(account) + finish() + } } /////////////////////////////////////////////////// @@ -1525,4 +1549,24 @@ class ActAccountSetting : AppCompatActivity(), .show() } } + + override fun onDialogDismissed(dialogId: Int) { + } + + override fun onColorSelected(dialogId: Int, newColor: Int) { + when (dialogId) { + COLOR_DIALOG_NOTIFICATION_ACCENT_COLOR -> { + account.notificationAccentColor = newColor or Color.BLACK + showNotificationColor() + saveUIToData() + } + else -> Unit + } + } + + private fun showNotificationColor() { + views.vNotificationAccentColorColor.backgroundColor = + account.notificationAccentColor.notZero() + ?: ContextCompat.getColor(this, R.color.colorOsNotificationAccent) + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPushMessageList.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPushMessageList.kt index 586c425a..a08abf85 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPushMessageList.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPushMessageList.kt @@ -15,7 +15,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import jp.juggler.subwaytooter.api.dialogOrToast -import jp.juggler.subwaytooter.api.entity.Acct import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding import jp.juggler.subwaytooter.databinding.LvPushMessageBinding import jp.juggler.subwaytooter.dialog.actionsDialog @@ -26,12 +25,14 @@ import jp.juggler.subwaytooter.push.pushRepo import jp.juggler.subwaytooter.table.PushMessage import jp.juggler.subwaytooter.table.daoAccountNotificationStatus import jp.juggler.subwaytooter.table.daoPushMessage +import jp.juggler.subwaytooter.table.daoSavedAccount import jp.juggler.subwaytooter.util.permissionSpecNotification import jp.juggler.subwaytooter.util.requester import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.data.encodeBase64Url import jp.juggler.util.data.notBlank +import jp.juggler.util.data.notZero import jp.juggler.util.log.LogCategory import jp.juggler.util.os.saveToDownload import jp.juggler.util.time.formatLocalTime @@ -59,6 +60,9 @@ class ActPushMessageList : AppCompatActivity() { private val prNotification = permissionSpecNotification.requester { // 特に何もしない } + private val acctMap by lazy { + daoSavedAccount.loadRealAccounts().associateBy { it.acct } + } override fun onCreate(savedInstanceState: Bundle?) { prNotification.register(this) @@ -96,7 +100,7 @@ class ActPushMessageList : AppCompatActivity() { launchAndShowError { actionsDialog { action(getString(R.string.push_message_re_decode)) { - pushRepo.reDecode(pm) + pushRepo.reprocess(pm) } action(getString(R.string.push_message_save_to_download_folder)) { export(pm) @@ -140,7 +144,7 @@ class ActPushMessageList : AppCompatActivity() { if (acct == null) { println("!!secret key is not exported because missing recepients acct.") } else { - val status = daoAccountNotificationStatus.load(Acct.parse(acct)) + val status = daoAccountNotificationStatus.load(acct) if (status == null) { println("!!secret key is not exported because missing status for acct $acct .") } else { @@ -157,12 +161,16 @@ class ActPushMessageList : AppCompatActivity() { private val tintIconMap = HashMap() - fun tintIcon(ic: PushMessageIconColor) = - tintIconMap.getOrPut(ic.name) { + fun tintIcon(pm: PushMessage, ic: PushMessageIconColor) = + tintIconMap.getOrPut("${ic.name}-${pm.loginAcct}") { val context = this val src = ContextCompat.getDrawable(context, ic.iconId)!! - DrawableCompat.wrap(src).also { - DrawableCompat.setTint(it, ContextCompat.getColor(context, ic.colorRes)) + DrawableCompat.wrap(src).also { d -> + val a = acctMap[pm.loginAcct] + val c = ic.colorRes.notZero()?.let { ContextCompat.getColor(context, it) } + ?: a?.notificationAccentColor?.notZero() + ?: ContextCompat.getColor(this, R.color.colorOsNotificationAccent) + DrawableCompat.setTint(d, c) } } @@ -183,10 +191,9 @@ class ActPushMessageList : AppCompatActivity() { pm ?: return lastItem = pm - val iconAndColor = pm.iconColor() Glide.with(views.ivSmall) .load(pm.iconSmall) - .error(tintIcon(iconAndColor)) + .error(tintIcon(pm, pm.iconColor())) .into(views.ivSmall) Glide.with(views.ivLarge) diff --git a/app/src/main/java/jp/juggler/subwaytooter/MyAppGlideModule.kt b/app/src/main/java/jp/juggler/subwaytooter/MyAppGlideModule.kt index b0eaed7c..148f8f59 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/MyAppGlideModule.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/MyAppGlideModule.kt @@ -1,22 +1,29 @@ package jp.juggler.subwaytooter import android.content.Context +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.PictureDrawable -import androidx.annotation.Nullable import com.bumptech.glide.Glide import com.bumptech.glide.GlideBuilder import com.bumptech.glide.Registry import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.integration.webp.decoder.* import com.bumptech.glide.load.Options import com.bumptech.glide.load.ResourceDecoder import com.bumptech.glide.load.engine.Resource +import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.resource.SimpleResource +import com.bumptech.glide.load.resource.bitmap.BitmapDrawableDecoder import com.bumptech.glide.load.resource.transcode.ResourceTranscoder import com.bumptech.glide.module.AppGlideModule import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVGParseException import java.io.IOException import java.io.InputStream +import java.nio.ByteBuffer import kotlin.math.min @GlideModule @@ -73,7 +80,6 @@ class MyAppGlideModule : AppGlideModule() { // Convert the [SVG]'s internal representation to an Android-compatible one ([Picture]). class SvgDrawableTranscoder : ResourceTranscoder { - @Nullable override fun transcode( toTranscode: Resource, options: Options, @@ -102,6 +108,65 @@ class MyAppGlideModule : AppGlideModule() { registry .register(SVG::class.java, PictureDrawable::class.java, SvgDrawableTranscoder()) .append(InputStream::class.java, SVG::class.java, SvgDecoder()) + + /////// + // Animated WebP + +// // We should put our decoder before the build-in decoders, +// // because the Downsampler will consume arbitrary data and make the inputstream corrupt +// // on some devices +// val resources: Resources = context.resources +// val bitmapPool: BitmapPool = glide.bitmapPool +// val arrayPool: ArrayPool = glide.arrayPool +// /* static webp decoders */ +// val webpDownsampler = WebpDownsampler( +// registry.imageHeaderParsers, +// resources.getDisplayMetrics(), bitmapPool, arrayPool +// ) +// val bitmapDecoder = AnimatedWebpBitmapDecoder(arrayPool, bitmapPool) +// val byteBufferBitmapDecoder = ByteBufferBitmapWebpDecoder(webpDownsampler) +// val streamBitmapDecoder = StreamBitmapWebpDecoder(webpDownsampler, arrayPool) +// /* animate webp decoders */ +// val byteBufferWebpDecoder = ByteBufferWebpDecoder(context, arrayPool, bitmapPool) +// registry /* Bitmaps for static webp images */ +// .prepend( +// Registry.BUCKET_BITMAP, +// ByteBuffer::class.java, +// Bitmap::class.java, byteBufferBitmapDecoder +// ) +// .prepend( +// Registry.BUCKET_BITMAP, +// InputStream::class.java, +// Bitmap::class.java, streamBitmapDecoder +// ) /* BitmapDrawables for static webp images */ +// .prepend( +// Registry.BUCKET_BITMAP_DRAWABLE, +// ByteBuffer::class.java, +// BitmapDrawable::class.java, +// BitmapDrawableDecoder(resources, byteBufferBitmapDecoder) +// ) +// .prepend( +// Registry.BUCKET_BITMAP_DRAWABLE, +// InputStream::class.java, +// BitmapDrawable::class.java, +// BitmapDrawableDecoder(resources, streamBitmapDecoder) +// ) /* Bitmaps for animated webp images*/ +// .prepend( +// Registry.BUCKET_BITMAP, +// ByteBuffer::class.java, +// Bitmap::class.java, ByteBufferAnimatedBitmapDecoder(bitmapDecoder) +// ) +// .prepend( +// Registry.BUCKET_BITMAP, +// InputStream::class.java, +// Bitmap::class.java, StreamAnimatedBitmapDecoder(bitmapDecoder) +// ) /* Animated webp images */ +// .prepend(ByteBuffer::class.java, WebpDrawable::class.java, byteBufferWebpDecoder) +// .prepend( +// InputStream::class.java, +// WebpDrawable::class.java, StreamWebpDecoder(byteBufferWebpDecoder, arrayPool) +// ) +// .prepend(WebpDrawable::class.java, WebpDrawableEncoder()) } override fun applyOptions(context: Context, builder: GlideBuilder) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt index 569911c5..614903a7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Account.kt @@ -70,7 +70,7 @@ fun ActMain.accountAdd() { val tootInstance = runApiTask2(apiHost) { TootInstance.getOrThrow(it) } addPseudoAccount(apiHost, tootInstance)?.let { a -> showToast(false, R.string.server_confirmed) - addColumn(defaultInsertPosition, a, ColumnType.LOCAL) + addColumn(defaultInsertPosition, a, ColumnType.LOCAL,protect=true) dialogHost.dismissSafe() } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt index 4f8791b1..1d0d6e10 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Boost.kt @@ -383,7 +383,7 @@ fun ActMain.clickBoostBy( columnType: ColumnType = ColumnType.BOOSTED_BY, ) { status ?: return - addColumn(false, pos, accessInfo, columnType, status.id) + addColumn(false, pos, accessInfo, columnType, params= arrayOf(status.id)) } fun ActMain.clickBoost(accessInfo: SavedAccount, status: TootStatus, willToast: Boolean) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt index 7ad6cddb..a49f8d5a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Conversation.kt @@ -164,7 +164,7 @@ fun ActMain.conversationLocal( else -> ColumnType.CONVERSATION }, - statusId, + params = arrayOf(statusId), ) private val reDetailedStatusTime = @@ -501,7 +501,10 @@ fun ActMain.conversationFromTootsearch( val replyId = status?.in_reply_to_id when { status == null -> showToast(true, result.error ?: "?") - replyId == null -> showToast(true, "showReplyTootsearch: in_reply_to_id is null") + replyId == null -> showToast( + true, + "showReplyTootsearch: in_reply_to_id is null" + ) else -> conversationLocal(pos, a, replyId) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_List.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_List.kt index 80756c1d..bbc8dcfb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_List.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_List.kt @@ -28,8 +28,10 @@ import okhttp3.Request fun ActMain.clickListTl(pos: Int, accessInfo: SavedAccount, item: TimelineItem?) { when (item) { - is TootList -> addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id) - is MisskeyAntenna -> addColumn(pos, accessInfo, ColumnType.MISSKEY_ANTENNA_TL, item.id) + is TootList -> + addColumn(pos, accessInfo, ColumnType.LIST_TL, params = arrayOf(item.id)) + is MisskeyAntenna -> + addColumn(pos, accessInfo, ColumnType.MISSKEY_ANTENNA_TL, params = arrayOf(item.id)) } } @@ -39,7 +41,7 @@ fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: Timeli launchAndShowError { actionsDialog(item.title) { action(getString(R.string.list_timeline)) { - addColumn(pos, accessInfo, ColumnType.LIST_TL, item.id) + addColumn(pos, accessInfo, ColumnType.LIST_TL, params = arrayOf(item.id)) } action(getString(R.string.list_member)) { addColumn( @@ -47,7 +49,7 @@ fun ActMain.clickListMoreButton(pos: Int, accessInfo: SavedAccount, item: Timeli pos, accessInfo, ColumnType.LIST_MEMBER, - item.id + params = arrayOf(item.id) ) } action(getString(R.string.rename)) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Notification.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Notification.kt index b922f794..d8b2c22d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Notification.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Notification.kt @@ -26,12 +26,7 @@ fun ActMain.clickNotificationFrom( showToast(false, R.string.misskey_account_not_supported) } else { accessInfo.getFullAcct(who).validFull()?.let { - addColumn( - pos, - accessInfo, - ColumnType.NOTIFICATION_FROM_ACCT, - it - ) + addColumn(pos, accessInfo, ColumnType.NOTIFICATION_FROM_ACCT, params = arrayOf(it)) } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Server.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Server.kt index 6945c8d3..8e8814dd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Server.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Server.kt @@ -59,7 +59,7 @@ private fun ActMain.serverProfileDirectory( pos, accessInfo, ColumnType.PROFILE_DIRECTORY, - host + params = arrayOf(host) ) // 疑似アカウントで開く @@ -70,7 +70,7 @@ private fun ActMain.serverProfileDirectory( pos, ai, ColumnType.PROFILE_DIRECTORY, - host + params = arrayOf(host) ) } } @@ -116,7 +116,7 @@ fun ActMain.serverInformation( pos, SavedAccount.na, ColumnType.INSTANCE_INFORMATION, - host + params = arrayOf(host), ) // ドメインブロック一覧から解除 diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Status.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Status.kt index f6b14f59..a7602ec8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Status.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Status.kt @@ -607,5 +607,5 @@ fun ActMain.openStatusHistory( accessInfo: SavedAccount, status: TootStatus, ) { - addColumn(pos, accessInfo, ColumnType.STATUS_HISTORY, status.id, status.json) + addColumn(pos, accessInfo, ColumnType.STATUS_HISTORY, params = arrayOf(status.id, status.json)) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt index 718e1398..9e771a30 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Tag.kt @@ -145,14 +145,13 @@ fun ActMain.tagTimeline( acctAscii: String? = null, ) { if (acctAscii == null) { - addColumn(pos, accessInfo, ColumnType.HASHTAG, tagWithoutSharp) + addColumn(pos, accessInfo, ColumnType.HASHTAG, params = arrayOf(tagWithoutSharp)) } else { addColumn( pos, accessInfo, ColumnType.HASHTAG_FROM_ACCT, - tagWithoutSharp, - acctAscii + params = arrayOf(tagWithoutSharp, acctAscii) ) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Timeline.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Timeline.kt index b8bd6500..ca3aff36 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_Timeline.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_Timeline.kt @@ -38,12 +38,14 @@ fun ActMain.timeline( )?.let { account -> when (type) { ColumnType.PROFILE -> - account.loginAccount?.id?.let { addColumn(pos, account, type, it) } + account.loginAccount?.id?.let { + addColumn(pos, account, type, params = arrayOf(it)) + } ColumnType.PROFILE_DIRECTORY -> - addColumn(pos, account, type, account.apiHost) + addColumn(pos, account, type, params = arrayOf(account.apiHost)) - else -> addColumn(pos, account, type, *args) + else -> addColumn(pos, account, type, params = args) } } } @@ -95,7 +97,7 @@ fun ActMain.timelineDomain( pos: Int, accessInfo: SavedAccount, host: Host, -) = addColumn(pos, accessInfo, ColumnType.DOMAIN_TIMELINE, host) +) = addColumn(pos, accessInfo, ColumnType.DOMAIN_TIMELINE, params = arrayOf(host)) // 指定タンスのローカルタイムラインを開く fun ActMain.timelineLocal( @@ -131,7 +133,7 @@ private fun ActMain.timelineAround( pos: Int, id: EntityId, type: ColumnType, -) = addColumn(pos, accessInfo, type, id) +) = addColumn(pos, accessInfo, type, params = arrayOf(id)) // 投稿を同期してstatusIdを調べてから指定アカウントでタイムラインを開く private fun ActMain.timelineAroundByStatus( diff --git a/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.kt b/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.kt index 9bb20647..e22b25f7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/action/Action_User.kt @@ -573,7 +573,7 @@ private fun ActMain.userProfileFromUrlOrAcct( openCustomTab(whoUrl) } - else -> addColumn(pos, accessInfo, ColumnType.PROFILE, who.id) + else -> addColumn(pos, accessInfo, ColumnType.PROFILE, params = arrayOf(who.id)) } } } @@ -597,7 +597,7 @@ fun ActMain.userProfileFromAnotherAccount( accountListArg = accountListNonPseudo(who.apDomain) )?.let { ai -> if (ai.matchHost(accessInfo)) { - addColumn(pos, ai, ColumnType.PROFILE, who.id) + addColumn(pos, ai, ColumnType.PROFILE, params = arrayOf(who.id)) } else { userProfileFromUrlOrAcct(pos, ai, accessInfo.getFullAcct(who), who.url) } @@ -613,7 +613,7 @@ fun ActMain.userProfileLocal( ) { when { accessInfo.isNA -> userProfileFromAnotherAccount(pos, accessInfo, who) - else -> addColumn(pos, accessInfo, ColumnType.PROFILE, who.id) + else -> addColumn(pos, accessInfo, ColumnType.PROFILE, params = arrayOf(who.id)) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt index be632897..b4cc7e2a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt @@ -77,7 +77,8 @@ fun ActMain.addColumn( indexArg: Int, ai: SavedAccount, type: ColumnType, - vararg params: Any, + protect: Boolean = false, + params: Array = emptyArray(), ): Column { if (!allowColumnDuplication) { // 既に同じカラムがあればそこに移動する @@ -90,7 +91,8 @@ fun ActMain.addColumn( } // - val col = Column(appState, ai, type.id, *params) + val col = Column(appState, ai, type.id, params) + if (protect) col.dontClose = true val index = addColumn(col, indexArg) scrollAndLoad(index) return col @@ -100,16 +102,16 @@ fun ActMain.addColumn( indexArg: Int, ai: SavedAccount, type: ColumnType, - vararg params: Any, -): Column { - return addColumn( - PrefB.bpAllowColumnDuplication.value, - indexArg, - ai, - type, - *params - ) -} + protect: Boolean = false, + params: Array = emptyArray(), +): Column = addColumn( + PrefB.bpAllowColumnDuplication.value, + indexArg, + ai, + type, + protect = protect, + params = params, +) fun ActMain.removeColumn(column: Column) { val idxColumn = appState.columnIndex(column) ?: return @@ -342,7 +344,7 @@ fun ActMain.searchFromActivityResult(data: Intent?, columnType: ColumnType) = defaultInsertPosition, SavedAccount.na, columnType, - it + params = arrayOf(it) ) } 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 03fef55c..27d55d7e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt @@ -320,11 +320,11 @@ private fun ActMain.afterAccountAdd( } // 適当にカラムを追加する - addColumn(false, defaultInsertPosition, account, ColumnType.HOME) + addColumn(false, defaultInsertPosition, account, ColumnType.HOME, protect = true) if (daoSavedAccount.isSingleAccount()) { - addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS) - addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL) - addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE) + addColumn(false, defaultInsertPosition, account, ColumnType.NOTIFICATIONS, protect = true) + addColumn(false, defaultInsertPosition, account, ColumnType.LOCAL, protect = true) + addColumn(false, defaultInsertPosition, account, ColumnType.FEDERATE, protect = true) } // 通知の更新が必要かもしれない diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt index 11c3438d..f465425d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt @@ -26,7 +26,10 @@ import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.pref.PrefB +import jp.juggler.subwaytooter.pref.PrefDevice.Companion.PUSH_DISTRIBUTOR_NONE import jp.juggler.subwaytooter.pref.PrefS +import jp.juggler.subwaytooter.pref.prefDevice +import jp.juggler.subwaytooter.push.fcmHandler import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.accountListCanSeeMyReactions import jp.juggler.subwaytooter.util.VersionString @@ -404,7 +407,12 @@ class SideMenuAdapter( // }, Item(icon = R.drawable.ic_search, title = R.string.notestock) { - addColumn(defaultInsertPosition, SavedAccount.na, ColumnType.SEARCH_NOTESTOCK, "") + addColumn( + defaultInsertPosition, + SavedAccount.na, + ColumnType.SEARCH_NOTESTOCK, + params = arrayOf("") + ) }, Item(), @@ -524,7 +532,8 @@ class SideMenuAdapter( ItemType.IT_NOTIFICATION_PERMISSION -> viewOrInflate(view, parent, R.layout.lv_sidemenu_item).apply { isAllCaps = false - text = actMain.getString(R.string.notification_permission_not_granted) + val action = notificationActionRecommend() ?: return@apply + text = actMain.getString(action.first) val drawable = createColoredDrawable(actMain, icon, iconColor, 1f) setCompoundDrawablesRelativeWithIntrinsicBounds( drawable, @@ -534,11 +543,8 @@ class SideMenuAdapter( ) setOnClickListener { drawer.closeDrawer(GravityCompat.START) - if (actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty()) { - actMain.prNotification.openAppSetting(actMain) - } else { - filterListItems() - } + notificationActionRecommend()?.second?.invoke() + filterListItems() } } } @@ -581,11 +587,24 @@ class SideMenuAdapter( this.notifyDataSetChanged() } + private fun notificationActionRecommend(): Pair Unit>? = when { + actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty() -> + Pair(R.string.notification_permission_not_granted) { + actMain.prNotification.openAppSetting(actMain) + } + (actMain.prefDevice.pushDistributor.isNullOrEmpty() && actMain.fcmHandler.noFcm) || + actMain.prefDevice.pushDistributor == PUSH_DISTRIBUTOR_NONE -> + Pair(R.string.notification_push_distributor_disabled) { + actMain.selectPushDistributor() + } + else -> null + } + fun filterListItems() { list = originalList.filter { when (it.itemType) { ItemType.IT_NOTIFICATION_PERMISSION -> - actMain.prNotification.spec.listNotGranded(actMain).isNotEmpty() + notificationActionRecommend() != null else -> true } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt index 325cf0ac..bb696fa9 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.kt @@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.* import jp.juggler.util.data.* @@ -603,7 +604,7 @@ suspend fun TootApiClient.syncAccountByUrl( }.toPostRequestBuilder() ) ?.apply { - ar = TootAccountRef.mayNull(parser, parser.account(jsonObject)) + ar = tootAccountRefOrNull(parser, parser.account(jsonObject)) if (ar == null && error == null) { setError(context.getString(R.string.user_id_conversion_failed)) } @@ -646,7 +647,7 @@ suspend fun TootApiClient.syncAccountByAcct( .toPostRequestBuilder() ) ?.apply { - ar = TootAccountRef.mayNull(parser, parser.account(jsonObject)) + ar = tootAccountRefOrNull(parser, parser.account(jsonObject)) if (ar == null && error == null) { setError(context.getString(R.string.user_id_conversion_failed)) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthRepo.kt b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthRepo.kt index 1d9c0100..cfce1f08 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthRepo.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthRepo.kt @@ -1,6 +1,7 @@ package jp.juggler.subwaytooter.api.auth import android.content.Context +import android.database.sqlite.SQLiteDatabase import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.entity.EntityId @@ -8,24 +9,26 @@ import jp.juggler.subwaytooter.notification.checkNotificationImmediate import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll import jp.juggler.subwaytooter.pref.PrefL import jp.juggler.subwaytooter.pref.lazyContext -import jp.juggler.subwaytooter.table.AcctColor -import jp.juggler.subwaytooter.table.SavedAccount -import jp.juggler.subwaytooter.table.appDatabase +import jp.juggler.subwaytooter.table.* import jp.juggler.util.log.LogCategory val Context.authRepo get() = AuthRepo( context = this, - daoAcctColor = AcctColor.Access(appDatabase), - daoSavedAccount = SavedAccount.Access(appDatabase, lazyContext), + database = appDatabase, ) class AuthRepo( private val context: Context = lazyContext, + private val database: SQLiteDatabase = appDatabase, private val daoAcctColor: AcctColor.Access = - AcctColor.Access(appDatabase), + AcctColor.Access(database), private val daoSavedAccount: SavedAccount.Access = - SavedAccount.Access(appDatabase, lazyContext), + SavedAccount.Access(database, context), + private val daoPushMessage: PushMessage.Access = + PushMessage.Access(database), + private val daoNotificationShown: NotificationShown.Access = + NotificationShown.Access(database), ) { companion object { private val log = LogCategory("AuthRepo") @@ -69,6 +72,8 @@ class AuthRepo( PrefL.lpTabletTootDefaultAccount.value = -1L } daoSavedAccount.delete(account.db_id) + daoPushMessage.deleteAccount(account.acct) + daoNotificationShown.cleayByAcct(account.acct) // appServerUnregister(context.applicationContextSafe, account) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/APTag.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/APTag.kt index c8d17bfe..0a8a6b97 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/APTag.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/APTag.kt @@ -38,7 +38,6 @@ class APTag(parser: TootParser, jsonArray: JsonArray?) { ) } else { emojiList[shortcode] = CustomEmoji( - apDomain = parser.apDomain, shortcode = shortcode, url = iconUrl, staticUrl = iconUrl, diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt index 75e684b3..604ad890 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/MisskeyNoteUpdate.kt @@ -4,11 +4,15 @@ import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.util.data.JsonObject import jp.juggler.util.log.LogCategory -class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) { - companion object { - private val log = LogCategory("MisskeyNoteUpdate") - } - +class MisskeyNoteUpdate( + val noteId: EntityId, + val type: Type, + var reaction: String? = null, + var userId: EntityId? = null, + var deletedAt: Long? = null, + var choice: Int? = null, + var emoji: CustomEmoji? = null, +) { enum class Type { REACTION, UNREACTION, @@ -16,53 +20,34 @@ class MisskeyNoteUpdate(apDomain: Host, apiHost: Host, src: JsonObject) { VOTED } - val noteId: EntityId - val type: Type - var reaction: String? = null - var userId: EntityId? = null - var deletedAt: Long? = null - var choice: Int? = null - var emoji: CustomEmoji? = null + companion object { - init { - noteId = EntityId.mayNull(src.string("id")) ?: error("MisskeyNoteUpdate: missing note id") + fun misskeyNoteUpdate(src: JsonObject): MisskeyNoteUpdate { - // root.body.body - val body2 = src.jsonObject("body") ?: error("MisskeyNoteUpdate: missing body") + val noteId = EntityId.mayNull(src.string("id")) + ?: error("MisskeyNoteUpdate: missing note id") - when (val strType = src.string("type")) { - "reacted" -> { - type = Type.REACTION - reaction = body2.string("reaction") - userId = EntityId.mayDefault(body2.string("userId")) - emoji = body2.jsonObject("emoji")?.let { - try { - CustomEmoji.decodeMisskey(apDomain, apiHost, it) - } catch (ex: Throwable) { - log.e(ex, "can't parse custom emoji.") - null - } - } + // root.body.body + val body2 = src.jsonObject("body") + ?: error("MisskeyNoteUpdate: missing body") + + val type: Type = when (val strType = src.string("type")) { + "reacted" -> Type.REACTION + "unreacted" -> Type.UNREACTION + "deleted" -> Type.DELETED + "pollVoted" -> Type.VOTED + else -> error("MisskeyNoteUpdate: unknown type $strType") } - "unreacted" -> { - type = Type.UNREACTION - reaction = body2.string("reaction") - userId = EntityId.mayDefault(body2.string("userId")) - } - - "deleted" -> { - type = Type.DELETED - deletedAt = TootStatus.parseTime(body2.string("deletedAt")) - } - - "pollVoted" -> { - type = Type.VOTED - choice = body2.int("choice") - userId = EntityId.mayDefault(body2.string("userId")) - } - - else -> error("MisskeyNoteUpdate: unknown type $strType") + return MisskeyNoteUpdate( + noteId = noteId, + type = type, + reaction = body2.string("reaction"), + userId = EntityId.mayNull(body2.string("userId")), + deletedAt = body2.string("deletedAt")?.let { TootStatus.parseTime(it) }, + choice = body2.int("choice"), + emoji = parseItem(body2.jsonObject("emoji"), CustomEmoji::decodeMisskey), + ) } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt index c2db6fe9..1fac7c37 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccount.kt @@ -7,6 +7,7 @@ import android.widget.TextView import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.MisskeyAccountDetailMap import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull import jp.juggler.subwaytooter.api.entity.TootStatus.Companion.tootStatus import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.pref.PrefB @@ -386,6 +387,8 @@ open class TootAccount( """\Ahttps://($reHostIdn)/users/(\w|\w+[\w-]*\w)(?=\z|[?#])""" .asciiPattern() + private val reMisskeyIoProxy = """\Ahttps://misskey\.io/proxy/""".toRegex() + fun tootAccount(parser: TootParser, src: JsonObject): TootAccount { src["_fromStream"] = parser.fromStream @@ -427,9 +430,7 @@ open class TootAccount( ServiceType.MISSKEY -> { custom_emojis = - parseMapOrNull(src.jsonArray("emojis")) { - CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it) - } + parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMisskey) profile_emojis = null username = src.stringOrThrow("username") @@ -476,17 +477,14 @@ open class TootAccount( created_at = src.string("createdAt") time_created_at = TootStatus.parseTime(created_at) - // https://github.com/syuilo/misskey/blob/develop/src/client/scripts/get-static-image-url.ts - fun String.getStaticImageUrl(): String? { - val uri = this.mayUri() ?: return null - val dummy = "${uri.encodedAuthority}${uri.encodedPath}" - return "https://${parser.linkHelper.apiHost.ascii}/proxy/$dummy?url=${encodePercent()}&static=1" - } - + // 画像を静止させるURLはAPIとしては提供されていない + // サーバ側で実装されている方法は仕様が安定しない + // クライアント側でアニメーションを止めるのが正解らしいが、 + // 対応できてないな… avatar = src.string("avatarUrl") - avatar_static = src.string("avatarUrl")?.getStaticImageUrl() + avatar_static = src.string("avatarUrl") header = src.string("bannerUrl") - header_static = src.string("bannerUrl")?.getStaticImageUrl() + header_static = src.string("bannerUrl") pinnedNoteIds = src.stringArrayList("pinnedNoteIds") if (parser.misskeyDecodeProfilePin) { @@ -561,13 +559,8 @@ open class TootAccount( else -> { // 絵文字データは先に読んでおく - custom_emojis = parseMapOrNull(src.jsonArray("emojis")) { - CustomEmoji.decode( - parser.apDomain, - parser.apiHost, - it - ) - } + custom_emojis = + parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMastodon) profile_emojis = when (val o = src["profile_emojis"]) { is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) } @@ -587,7 +580,7 @@ open class TootAccount( note = src.string("note") source = parseSource(src.jsonObject("source")) - movedRef = TootAccountRef.mayNull( + movedRef = tootAccountRefOrNull( parser, src.jsonObject("moved")?.let { tootAccount(parser, it) @@ -692,8 +685,8 @@ open class TootAccount( acct = acct, apDomain = apDomain, apiHost = apiHost, - avatar = avatar, - avatar_static = avatar_static, + avatar = avatar?.replace(reMisskeyIoProxy, "https://"), + avatar_static = avatar_static?.replace(reMisskeyIoProxy, "https://"), birthday = birthday, bot = bot, created_at = created_at, diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccountRef.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccountRef.kt index cd5b5cac..06b3f045 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccountRef.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAccountRef.kt @@ -18,10 +18,7 @@ class TootAccountRef private constructor( fun get() = TootAccountMap.find(this) companion object { - fun notNull(parser: TootParser, account: TootAccount) = - tootAccountRef(parser, account) - - fun mayNull(parser: TootParser, account: TootAccount?): TootAccountRef? { + fun tootAccountRefOrNull(parser: TootParser, account: TootAccount?): TootAccountRef? { return when (account) { null -> null else -> tootAccountRef(parser, account) diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt index 7e7112cb..6754c4ee 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAnnouncement.kt @@ -38,9 +38,7 @@ class TootAnnouncement( private val log = LogCategory("TootAnnouncement") fun tootAnnouncement(parser: TootParser, src: JsonObject): TootAnnouncement { - val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) { - CustomEmoji.decode(parser.apDomain, parser.apiHost, it) - } + val custom_emojis = parseMapOrNull(src.jsonArray("emojis"), CustomEmoji::decodeMastodon) val reactions = parseListOrNull(src.jsonArray("reactions")) { TootReaction.parseFedibird(it) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.kt index d314cbb8..7b964a67 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.kt @@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.api.entity import android.content.Context import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.util.data.JsonObject import jp.juggler.util.data.notEmpty @@ -81,90 +82,79 @@ class TootNotification( const val TYPE_STATUS_REFERENCE = "status_reference" const val TYPE_SCHEDULED_STATUS = "scheduled_status" - fun tootNotification(parser: TootParser, src: JsonObject): TootNotification { - val id: EntityId - // One of: "mention", "reblog", "favourite", "follow" - val type: String - // The Account sending the notification to the user - val accountRef: TootAccountRef? + private fun tootNotificationMisskey(parser: TootParser, src: JsonObject): TootNotification { + // Misskeyの通知APIはページネーションをIDでしか行えない + // これは改善される予定 https://github.com/syuilo/misskey/issues/2275 - // The Status associated with the notification, if applicable - // 投稿の更新により変更可能になる - val status: TootStatus? + val created_at: String? = src.string("createdAt") - val reaction: TootReaction? + val accountRef = tootAccountRefOrNull( + parser, + parser.account(src.jsonObject("user")) + ) - val reblog_visibility: TootVisibility + val reaction: TootReaction? = src.string("reaction") + ?.notEmpty() + ?.let { TootReaction.parseMisskey(it) } - // The time the notification was created - val created_at: String? - val time_created_at: Long - - - if (parser.serviceType == ServiceType.MISSKEY) { - id = EntityId.mayDefault(src.string("id")) - - type = src.stringOrThrow("type") - - created_at = src.string("createdAt") - time_created_at = TootStatus.parseTime(created_at) - - accountRef = TootAccountRef.mayNull( - parser, - parser.account( - src.jsonObject("user") - ) - ) - status = parser.status( - src.jsonObject("note") - ) - - reaction = src.string("reaction") - ?.notEmpty() - ?.let { TootReaction.parseMisskey(it) } - - reblog_visibility = TootVisibility.Unknown - - // Misskeyの通知APIはページネーションをIDでしか行えない - // これは改善される予定 https://github.com/syuilo/misskey/issues/2275 - } else { - id = EntityId.mayDefault(src.string("id")) - - type = src.stringOrThrow("type") - - created_at = src.string("created_at") - time_created_at = TootStatus.parseTime(created_at) - accountRef = - TootAccountRef.mayNull(parser, parser.account(src.jsonObject("account"))) - status = parser.status(src.jsonObject("status")) - - reaction = src.jsonObject("emoji_reaction") - ?.notEmpty() - ?.let { TootReaction.parseFedibird(it) } - // pleroma unicode emoji - ?: src.string("emoji")?.let { TootReaction(name = it) } - - // fedibird - // https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9 - val visibilityString = when { - src.boolean("limited") == true -> "limited" - else -> src.string("reblog_visibility") - } - reblog_visibility = TootVisibility.parseMastodon(visibilityString) - ?: TootVisibility.Unknown - } return TootNotification( json = src, - id = id, - type = type, + id = EntityId.mayDefault(src.string("id")), + type = src.stringOrThrow("type"), + accountRef = accountRef, + status = parser.status(src.jsonObject("note")), + reaction = reaction, + reblog_visibility = TootVisibility.Unknown, + created_at = created_at, + time_created_at = TootStatus.parseTime(created_at), + ) + } + + private fun tootNotificationMastodon( + parser: TootParser, + src: JsonObject, + ): TootNotification { + + val created_at: String? = src.string("created_at") + + val accountRef: TootAccountRef? = + tootAccountRefOrNull(parser, parser.account(src.jsonObject("account"))) + + val status: TootStatus? = parser.status(src.jsonObject("status")) + + val reaction: TootReaction? = src.jsonObject("emoji_reaction") + ?.notEmpty() + ?.let { TootReaction.parseFedibird(it) } + ?: src.string("emoji")?.let { TootReaction(name = it) } // pleroma unicode emoji + + // fedibird + // https://github.com/fedibird/mastodon/blob/7974fd3c7ec11ea9f7bef4ad7f4009fff53f62af/app/serializers/rest/notification_serializer.rb#L9 + val visibilityString = when { + src.boolean("limited") == true -> "limited" + else -> src.string("reblog_visibility") + } + + val reblog_visibility = TootVisibility.parseMastodon(visibilityString) + ?: TootVisibility.Unknown + + return TootNotification( + json = src, + id = EntityId.mayDefault(src.string("id")), + type = src.stringOrThrow("type"), accountRef = accountRef, status = status, reaction = reaction, reblog_visibility = reblog_visibility, created_at = created_at, - time_created_at = time_created_at, + time_created_at = TootStatus.parseTime(created_at), ) } + + fun tootNotification(parser: TootParser, src: JsonObject): TootNotification = + when (parser.serviceType) { + ServiceType.MISSKEY -> tootNotificationMisskey(parser, src) + else -> tootNotificationMastodon(parser, src) + } } override fun getOrderId() = id @@ -172,16 +162,8 @@ class TootNotification( fun getNotificationLine(context: Context): String { val name = when (PrefB.bpShowAcctInSystemNotification.value) { - false -> accountRef?.decoded_display_name - - true -> { - val acctPretty = accountRef?.get()?.acct?.pretty - if (acctPretty?.isNotEmpty() == true) { - "@$acctPretty" - } else { - null - } - } + true -> accountRef?.get()?.acct?.pretty?.notEmpty()?.let { "@$it" } + else -> accountRef?.decoded_display_name } ?: "?" return when (type) { @@ -223,7 +205,10 @@ class TootNotification( TYPE_EMOJI_REACTION_PLEROMA, TYPE_EMOJI_REACTION, TYPE_REACTION, - -> context.getString(R.string.display_name_reaction_by, name) + -> arrayOf( + context.getString(R.string.display_name_reaction_by, name), + reaction?.name + ).mapNotNull { it.notEmpty() }.joinToString(" ") TYPE_VOTE, TYPE_POLL_VOTE_MISSKEY, @@ -239,7 +224,7 @@ class TootNotification( TYPE_POLL, -> context.getString(R.string.end_of_polling_from, name) - else -> "?" + else -> context.getString(R.string.unknown_notification_from, name) + " :" + type } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt index fbae7ff5..2d15a990 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootStatus.kt @@ -9,6 +9,7 @@ import androidx.annotation.StringRes import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.TootAccountMap import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef import jp.juggler.subwaytooter.api.entity.TootAttachment.Companion.tootAttachment import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.mfm.SpannableStringBuilderEx @@ -113,9 +114,9 @@ class TootStatus( //Application from which the status was posted val application: TootApplication?, - var custom_emojis: HashMap? = null, + var custom_emojis: MutableMap?, - val profile_emojis: HashMap?, + val profile_emojis: Map?, // The time the status was created private val created_at: String?, @@ -569,12 +570,19 @@ class TootStatus( } val who = parser.account(src.jsonObject("user")) ?: error("missing account") - val accountRef = TootAccountRef.tootAccountRef(parser, who) + val accountRef = tootAccountRef(parser, who) val account = accountRef.get() val created_at = src.string("createdAt") // 絵文字マップはすぐ後で使うので、最初の方で読んでおく - val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) { - CustomEmoji.decodeMisskey(parser.apDomain, parser.apiHost, it) + var custom_emojis: MutableMap? = + parseMapOrNull(src.jsonArray("emojis"),CustomEmoji::decodeMisskey) + val reactionEmojis: MutableMap? = + CustomEmoji.decodeMisskey12ReactionEmojis(src.jsonObject("reactionEmojis")) + if (reactionEmojis != null) { + custom_emojis = when (custom_emojis) { + null -> reactionEmojis + else -> (reactionEmojis + custom_emojis).toMutableMap() + } } // Misskeyは画像毎にNSFWフラグがある。どれか1枚でもNSFWならトゥート全体がNSFWということにする @@ -941,9 +949,7 @@ class TootStatus( val created_at = src.string("created_at") // 絵文字マップはすぐ後で使うので、最初の方で読んでおく - val custom_emojis = parseMapOrNull(src.jsonArray("emojis")) { - CustomEmoji.decode(parser.apDomain, parser.apiHost, it) - } + val custom_emojis = parseMapOrNull(src.jsonArray("emojis"),CustomEmoji::decodeMastodon) val profile_emojis = when (val o = src["profile_emojis"]) { is JsonArray -> parseMapOrNull(o) { NicoProfileEmoji(it) } @@ -981,7 +987,7 @@ class TootStatus( time_created_at = parseTime(created_at) media_attachments = parseListOrNull(src.jsonArray("media_attachments")) { - tootAttachment(parser,it) + tootAttachment(parser, it) } val visibilityString = when { src.boolean("limited") == true -> "limited" diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt b/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt index caa6f675..4e0fa728 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/finder/DataFinders.kt @@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.api.finder import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.data.JsonArray import jp.juggler.util.data.JsonObject @@ -23,7 +24,7 @@ private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray, srcList.objectList().mapNotNull { when (val relationId = EntityId.mayNull(it.string("id"))) { null -> null - else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key))) + else -> tootAccountRefOrNull(parser, parser.account(it.jsonObject(key))) ?.apply { _orderId = relationId } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/Column.kt b/app/src/main/java/jp/juggler/subwaytooter/column/Column.kt index 46537ba3..7bd635a2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/Column.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/Column.kt @@ -327,7 +327,7 @@ class Column( appState: AppState, accessInfo: SavedAccount, type: Int, - vararg params: Any, + params: Array, ) : this( appState = appState, context = appState.context, diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt index 488ffb7d..d126b6db 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnExtra2.kt @@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull import jp.juggler.subwaytooter.columnviewholder.saveScrollPosition import jp.juggler.util.data.* import jp.juggler.util.log.LogCategory @@ -122,7 +123,7 @@ suspend fun Column.loadProfileAccount( // ユーザリレーションの取り扱いのため、別のparserを作ってはいけない parser.misskeyDecodeProfilePin = true try { - TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a -> + tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a -> this.whoAccount = a client.publishApiProgress("") // カラムヘッダの再表示 } @@ -134,7 +135,7 @@ suspend fun Column.loadProfileAccount( else -> client.request( "/api/v1/accounts/$profileId" )?.also { result1 -> - TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a -> + tootAccountRefOrNull(parser, parser.account(result1.jsonObject))?.also { a -> this.whoAccount = a this.whoFeaturedTags = null diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgConfirm.kt b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgConfirm.kt index 07e132fb..7c33213a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgConfirm.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgConfirm.kt @@ -95,21 +95,22 @@ object DlgConfirm { suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) = confirm(getString(messageId, *args)) - suspend fun AppCompatActivity.confirm(message: CharSequence) { + suspend fun AppCompatActivity.confirm(message: CharSequence, title: CharSequence? = null) { suspendCancellableCoroutine { cont -> try { val views = DlgConfirmBinding.inflate(layoutInflater) views.tvMessage.text = message views.cbSkipNext.visibility = View.GONE - val dialog = AlertDialog.Builder(this) - .setView(views.root) - .setCancelable(true) - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.ok) { _, _ -> + val dialog = AlertDialog.Builder(this).apply { + setView(views.root) + setCancelable(true) + title?.let { setTitle(it) } + setNegativeButton(R.string.cancel, null) + setPositiveButton(R.string.ok) { _, _ -> if (cont.isActive) cont.resume(Unit) } - .create() + }.create() dialog.setOnDismissListener { if (cont.isActive) cont.resumeWithException(CancellationException("dialog closed.")) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt index 26f2b264..6729699c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/emoji/EmojiBase.kt @@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.util.data.JsonArray import jp.juggler.util.data.JsonObject import jp.juggler.util.data.notEmpty +import jp.juggler.util.data.toMutableMap sealed interface EmojiBase @@ -52,7 +53,6 @@ class UnicodeEmoji( } class CustomEmoji( - val apDomain: Host, val shortcode: String, // shortcode (コロンを含まない) val url: String, // 画像URL val staticUrl: String?, // アニメーションなしの画像URL @@ -63,7 +63,6 @@ class CustomEmoji( ) : EmojiBase, Mappable { fun makeAlias(alias: String) = CustomEmoji( - apDomain = apDomain, shortcode = shortcode, url = url, staticUrl = staticUrl, @@ -80,9 +79,8 @@ class CustomEmoji( companion object { - val decode: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src -> - CustomEmoji( - apDomain = apDomain, + fun decodeMastodon(src: JsonObject): CustomEmoji { + return CustomEmoji( shortcode = src.stringOrThrow("shortcode"), url = src.stringOrThrow("url"), staticUrl = src.string("static_url"), @@ -91,24 +89,20 @@ class CustomEmoji( ) } - val decodeMisskey: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, _, src -> + fun decodeMisskey(src: JsonObject): CustomEmoji { val url = src.string("url") ?: error("missing url") - - CustomEmoji( - apDomain = apDomain, + return CustomEmoji( shortcode = src.string("name") ?: error("missing name"), url = url, staticUrl = url, - aliases = parseAliases(src.jsonArray("aliases")), category = src.string("category"), ) } - val decodeMisskey13: (Host, Host, JsonObject) -> CustomEmoji = { apDomain, apiHost, src -> + fun decodeMisskey13(apiHost: Host, src: JsonObject): CustomEmoji { val name = src.string("name") ?: error("missing name") val url = "https://${apiHost.ascii}/emoji/$name.webp" - CustomEmoji( - apDomain = apDomain, + return CustomEmoji( shortcode = name, url = url, staticUrl = url, @@ -117,6 +111,20 @@ class CustomEmoji( ) } + // 入力は name→URLの単純なマップ + fun decodeMisskey12ReactionEmojis(src: JsonObject?): MutableMap? = + src?.entries?.mapNotNull { + val (k, v) = it + when (val url = (v as? String)) { + null, "" -> null + else -> k to CustomEmoji( + shortcode = k, + url = url, + staticUrl = url + (if (url.contains('?')) '&' else '?') + "static=1", + ) + } + }?.notEmpty()?.toMutableMap() + private fun parseAliases(src: JsonArray?): ArrayList? { var dst = null as ArrayList? if (src != 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 9e9afe5a..6a57d207 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/NotificationChannels.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/NotificationChannels.kt @@ -121,7 +121,7 @@ enum class NotificationChannels( ; - fun isDissabled(context: Context) = !isEnabled(context) + fun isDisabled(context: Context) = !isEnabled(context) fun isEnabled(context: Context): Boolean { if (Build.VERSION.SDK_INT >= 33) { @@ -175,22 +175,26 @@ enum class NotificationChannels( text: String? = context.getString(descId), piTap: PendingIntent? = null, piDelete: PendingIntent? = null, + force:Boolean = false, ): ForegroundInfo? { - if (Build.VERSION.SDK_INT >= 33) { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) != PackageManager.PERMISSION_GRANTED - ) { - log.w("[$id] missing POST_NOTIFICATIONS.") + val notificationManager = NotificationManagerCompat.from(context) + + if(!force){ + if (Build.VERSION.SDK_INT >= 33) { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) != PackageManager.PERMISSION_GRANTED + ) { + log.w("[$id] missing POST_NOTIFICATIONS.") + return null + } + } + if (!notificationManager.isChannelEnabled(id)) { + log.w("[$id] notification channel is disabled.") return null } } - val notificationManager = NotificationManagerCompat.from(context) - if (!notificationManager.isChannelEnabled(id)) { - log.w("[$id] notification channel is disabled.") - return null - } val nc = this val builder = NotificationCompat.Builder(context, nc.id).apply { priority = nc.priority diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingChecker.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingChecker.kt index b9234f4e..4747984d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingChecker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingChecker.kt @@ -328,6 +328,13 @@ class PollingChecker( val notification = parser.notification(src) ?: return + // プッシュ通知で既出なら通知しない + // プルの場合同じ通知が何度もここを通るので、既出フラグを立てない + if (daoNotificationShown.isDuplicate(account.acct, notification.id.toString())) { + log.i("update_sub: skip duplicate. ${account.acct} ${notification.id}") + return + } + // アプリミュートと単語ミュート if (notification.status?.checkMuted() == true) return diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingUtils.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingUtils.kt index f2eb02f2..18b88688 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingUtils.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingUtils.kt @@ -58,7 +58,7 @@ fun AppCompatActivity.resetNotificationTracking(account: SavedAccount) { } launchAndShowError { withContext(AppDispatchers.IO){ - daoNotificationShown.cleayByAcct(account.acct.ascii) + daoNotificationShown.cleayByAcct(account.acct) PollingChecker.accountMutex(account.db_id).withLock { daoNotificationTracking.resetTrackingState(account.db_id) } @@ -232,7 +232,7 @@ suspend fun checkNoticifationAll( } } - daoSavedAccount.loadAccountList().mapNotNull { sa -> + daoSavedAccount.loadRealAccounts().mapNotNull { sa -> when { sa.isPseudo || !sa.isConfirmed -> null else -> EmptyScope.launch(AppDispatchers.DEFAULT) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker2.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker2.kt index 1b34df84..ff81a4fa 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker2.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker2.kt @@ -158,9 +158,10 @@ class PollingWorker2( private fun messageToForegroundInfo( text: String, + force:Boolean =false ): ForegroundInfo? { // テキストが変化していないなら更新しない - if (text.isEmpty() || text == lastMessage) return null + if (!force && (text.isEmpty() || text == lastMessage)) return null lastMessage = text log.i(text) @@ -181,6 +182,15 @@ class PollingWorker2( context, text = text, piTap = piTap, + force = force, ) } + + /** + * ワーカーの初期化時にOSから呼ばれる場合がある + * - Android 11 moto g31 で発生 + * - ダミーメッセージを仕込んだForegroundInfoを返す + */ + override suspend fun getForegroundInfo(): ForegroundInfo = + messageToForegroundInfo("initializing…",force=true)!! } diff --git a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt index e27c8c52..b9f71ec8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt @@ -189,7 +189,7 @@ object PrefB { ) val bpMoveNotificationsQuickFilter = BooleanPref( "MoveNotificationsQuickFilter", - false + true ) val bpShowAcctInSystemNotification = BooleanPref( "ShowAcctInSystemNotification", diff --git a/app/src/main/java/jp/juggler/subwaytooter/push/PushMessageIconColor.kt b/app/src/main/java/jp/juggler/subwaytooter/push/PushMessageIconColor.kt index b3c470d9..874f09a4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushMessageIconColor.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushMessageIconColor.kt @@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.push import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import jp.juggler.subwaytooter.R +import jp.juggler.subwaytooter.api.entity.TootNotification import jp.juggler.subwaytooter.table.PushMessage import jp.juggler.util.log.LogCategory @@ -14,42 +15,42 @@ enum class PushMessageIconColor( val keys: Array, ) { Favourite( - R.color.colorNotificationAccentFavourite, + 0, R.drawable.ic_star_outline, arrayOf("favourite"), ), Mention( - R.color.colorNotificationAccentMention, + 0, R.drawable.outline_alternate_email_24, arrayOf("mention"), ), Reply( - R.color.colorNotificationAccentReply, + 0, R.drawable.ic_reply, arrayOf("reply") ), Reblog( - R.color.colorNotificationAccentReblog, + 0, R.drawable.ic_repeat, arrayOf("reblog", "renote"), ), Quote( - R.color.colorNotificationAccentQuote, + 0, R.drawable.ic_quote, arrayOf("quote"), ), Follow( - R.color.colorNotificationAccentFollow, + 0, R.drawable.ic_person_add, arrayOf("follow", "followRequestAccepted") ), Unfollow( - R.color.colorNotificationAccentUnfollow, + 0, R.drawable.ic_follow_cross, arrayOf("unfollow") ), Reaction( - R.color.colorNotificationAccentReaction, + 0, R.drawable.outline_add_reaction_24, arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction") ), @@ -59,25 +60,30 @@ enum class PushMessageIconColor( arrayOf("follow_request", "receiveFollowRequest"), ), Poll( - R.color.colorNotificationAccentPoll, + 0, R.drawable.outline_poll_24, arrayOf("pollVote", "poll_vote", "poll"), ), Status( - R.color.colorNotificationAccentStatus, + 0, R.drawable.ic_edit, arrayOf("status", "update", "status_reference") ), - SignUp( - R.color.colorNotificationAccentSignUp, + AdminSignUp( + 0, R.drawable.outline_group_add_24, - arrayOf("admin.sign_up"), + arrayOf(TootNotification.TYPE_ADMIN_SIGNUP), + ), + AdminReport( + R.color.colorNotificationAccentAdminReport, + R.drawable.ic_error, + arrayOf(TootNotification.TYPE_ADMIN_REPORT), ), Unknown( R.color.colorNotificationAccentUnknown, R.drawable.ic_question, - arrayOf("unknown", "admin.sign_up"), + arrayOf("unknown"), ) ; diff --git a/app/src/main/java/jp/juggler/subwaytooter/push/PushMisskey.kt b/app/src/main/java/jp/juggler/subwaytooter/push/PushMisskey.kt index 780cad67..d7b24b03 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushMisskey.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushMisskey.kt @@ -7,8 +7,10 @@ import jp.juggler.crypt.generateKeyPair import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.ApiError import jp.juggler.subwaytooter.api.TootParser +import jp.juggler.subwaytooter.api.entity.TootAccount.Companion.tootAccount import jp.juggler.subwaytooter.api.entity.TootNotification import jp.juggler.subwaytooter.api.entity.TootStatus +import jp.juggler.subwaytooter.api.entity.parseItem import jp.juggler.subwaytooter.api.push.ApiPushMisskey import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.lazyContext @@ -172,11 +174,28 @@ class PushMisskey( when (val eventType = json.string("type")) { "notification" -> { + val body = json.jsonObject("body") + ?: error("missing body of notification") val parser = TootParser(context, a) - val notification = parser.notification(json.jsonObject("body")) - ?: error("can't parse notification. json=$json") - val user = notification.account + val whoJson = body.jsonObject("user") + var who = parseItem(whoJson) { tootAccount(parser, it) } + + body.jsonObject("note")?.let { noteJson -> + if (noteJson["user"] == null) { + noteJson["user"] = when (noteJson.string("userId")) { + null, "" -> null + who?.id?.toString() -> whoJson + a.loginAccount?.id?.toString() -> a.loginAccount?.json + else -> null + } + } + } + + val notification = parser.notification(body) + ?: error("can't parse notification. json=$body") + + who = notification.account // アプリミュートと単語ミュート if (notification.status?.checkMuted() == true) { @@ -191,7 +210,7 @@ class PushMisskey( TootNotification.TYPE_FOLLOW_REQUEST, TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY, -> { - val whoAcct = a.getFullAcct(user) + val whoAcct = a.getFullAcct(who) if (TootStatus.favMuteSet?.contains(whoAcct) == true) { error("muted by favMuteSet ${whoAcct.pretty}") } @@ -200,8 +219,9 @@ class PushMisskey( // バッジ画像のURLはない。通知種別により決まる pm.iconSmall = null - pm.iconLarge = a.supplyBaseUrl(user?.avatar_static) + pm.iconLarge = a.supplyBaseUrl(who?.avatar_static) pm.notificationType = notification.type + pm.notificationId = notification.id.toString() json.long("dateTime")?.let { pm.timestamp = it } @@ -224,3 +244,67 @@ class PushMisskey( } } } +/* + +Misskey13 +{ + "type": "notification", + "body": { + "id": "9ayflq5wj4", + "createdAt": "2023-02-07T23:22:38.132Z", + "type": "reaction", + "isRead": false, + "userId": "80jbzppr37", + "user": { + "id": "80jbzppr37", + "name": "tateisu🔧", + "username": "tateisu", + "host": "fedibird.com", + "avatarUrl": "https://nos3.arkjp.net/avatar.webp?url=https%3A%2F%2Fs3.fedibird.com%2Faccounts%2Favatars%2F000%2F010%2F223%2Foriginal%2Fb7ace6ef7eaaf49f.png&avatar=1", + "avatarBlurhash": "yMMHS-t71NWX~qx]%2yEf6i_kCoKn%M{tSkCoJaeM{ayoeyEWBxtt7IAWBWqShkCi_WBt7jZRkMxayt6aeWray%Mxvj[oeofM|WBRj", + "isBot": false, + "isCat": false, + "instance": { + "name": "Fedibird", + "softwareName": "fedibird", + "softwareVersion": "0.1", + "iconUrl": "https://fedibird.com/android-chrome-192x192.png", + "faviconUrl": "https://fedibird.com/favicon.ico", + "themeColor": "#282c37" + }, + "emojis": {}, + "onlineStatus": "unknown" + }, + "note": { + "id": "9aybef5b1d", + "createdAt": "2023-02-07T21:24:58.799Z", + "userId": "7rm6y6thc1", + "text": "(📎1)", + "visibility": "public", + "localOnly": false, + "renoteCount": 0, + "repliesCount": 0, + "reactions": { + "👍": 1, + ":kakkoii@.:": 1, + ":utsukushii@.:": 1 + }, + "reactionEmojis": { + "blobcatlobster_MUDAMUDAMUDA@fedibird.com": "https://nos3.arkjp.net/emoji.webp?url=https%3A%2F%2Fs3.fedibird.com%2Fcustom_emojis%2Fimages%2F000%2F151%2F856%2Foriginal%2F936dd0a34673cb19.png" + }, + "fileIds": [ + "9aybedosdl" + ], + "files": [...], + ], + "replyId": null, + "renoteId": null + }, + "reaction": "👍" + }, + "userId": "7rm6y6thc1", + "dateTime": 1675812160174 +} + + + */ \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/push/PushRepo.kt b/app/src/main/java/jp/juggler/subwaytooter/push/PushRepo.kt index d8afee05..46198dcc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushRepo.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushRepo.kt @@ -12,7 +12,6 @@ import androidx.work.await import jp.juggler.crypt.* import jp.juggler.subwaytooter.ActCallback import jp.juggler.subwaytooter.R -import jp.juggler.subwaytooter.api.entity.Acct import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.push.ApiPushAppServer import jp.juggler.subwaytooter.api.push.ApiPushMastodon @@ -46,19 +45,23 @@ import java.util.concurrent.TimeUnit private val log = LogCategory("PushRepo") +private val defaultOkHttp by lazy { + OkHttpClient.Builder().apply { + connectTimeout(60, TimeUnit.SECONDS) + writeTimeout(60, TimeUnit.SECONDS) + readTimeout(60, TimeUnit.SECONDS) + }.build() +} + val Context.pushRepo: PushRepo get() { - val okHttp = OkHttpClient.Builder().apply { - connectTimeout(60, TimeUnit.SECONDS) - writeTimeout(60, TimeUnit.SECONDS) - readTimeout(60, TimeUnit.SECONDS) - }.build() + val okHttp = defaultOkHttp val appDatabase = appDatabase return PushRepo( context = applicationContextSafe, - apiPushAppServer = ApiPushAppServer(okHttp), - apiPushMastodon = ApiPushMastodon(okHttp), - apiPushMisskey = ApiPushMisskey(okHttp), + apiAppServer = ApiPushAppServer(okHttp), + apiMastodon = ApiPushMastodon(okHttp), + apiMisskey = ApiPushMisskey(okHttp), daoSavedAccount = SavedAccount.Access(appDatabase, this), daoPushMessage = PushMessage.Access(appDatabase), daoStatus = AccountNotificationStatus.Access(appDatabase), @@ -70,9 +73,9 @@ val Context.pushRepo: PushRepo class PushRepo( private val context: Context, - private val apiPushMastodon: ApiPushMastodon, - private val apiPushMisskey: ApiPushMisskey, - private val apiPushAppServer: ApiPushAppServer, + private val apiMastodon: ApiPushMastodon, + private val apiMisskey: ApiPushMisskey, + private val apiAppServer: ApiPushAppServer, private val daoSavedAccount: SavedAccount.Access, private val daoPushMessage: PushMessage.Access, private val daoStatus: AccountNotificationStatus.Access, @@ -92,7 +95,7 @@ class PushRepo( private val pushMisskey by lazy { PushMisskey( context = context, - api = apiPushMisskey, + api = apiMisskey, provider = provider, prefDevice = prefDevice, daoStatus = daoStatus, @@ -102,7 +105,7 @@ class PushRepo( private val pushMastodon by lazy { PushMastodon( context = context, - api = apiPushMastodon, + api = apiMastodon, provider = provider, prefDevice = prefDevice, daoStatus = daoStatus, @@ -220,7 +223,7 @@ class PushRepo( prefDevice.fcmTokenExpired.notEmpty()?.let { refReporter?.get()?.setMessage("期限切れのFCMデバイストークンをアプリサーバから削除しています") log.i("remove fcmTokenExpired") - apiPushAppServer.endpointRemove(fcmToken = it) + apiAppServer.endpointRemove(fcmToken = it) prefDevice.fcmTokenExpired = null } } catch (ex: Throwable) { @@ -232,15 +235,15 @@ class PushRepo( prefDevice.upEndpointExpired.notEmpty()?.let { refReporter?.get()?.setMessage("期限切れのUnifiedPushエンドポイントをアプリサーバから削除しています") log.i("remove upEndpointExpired") - apiPushAppServer.endpointRemove(upUrl = it) + apiAppServer.endpointRemove(upUrl = it) prefDevice.upEndpointExpired = null } } catch (ex: Throwable) { log.w(ex, "can't forgot upEndpointExpired") } - val realAccounts = daoSavedAccount.loadAccountList() - .filter { !it.isPseudo } + val realAccounts = daoSavedAccount.loadRealAccounts() + .filter { !it.isPseudo && it.isConfirmed } val accts = realAccounts.map { it.acct } @@ -269,23 +272,34 @@ class PushRepo( prefDevice.pushDistributor = null } - log.i("pushDistributor=${prefDevice.pushDistributor}") val acctHashList = acctHashMap.keys.toList() + val json = when (prefDevice.pushDistributor) { null, "" -> when { - fcmHandler.hasFcm -> registerEndpointFcm(acctHashList) + fcmHandler.hasFcm -> { + log.i("registerEndpoint dist=FCM(default), acctHashList=${acctHashList.size}") + registerEndpointFcm(acctHashList) + } else -> { log.w("pushDistributor not selected. but can't select default distributor from background service.") - null + return } } PrefDevice.PUSH_DISTRIBUTOR_NONE -> { + log.i("push distrobuter 'none' is selected. it will remove subscription.") willRemoveSubscription = true null } - PrefDevice.PUSH_DISTRIBUTOR_FCM -> registerEndpointFcm(acctHashList) - else -> registerEndpointUnifiedPush(acctHashList) + PrefDevice.PUSH_DISTRIBUTOR_FCM -> { + log.i("registerEndpoint dist=FCM, acctHashList=${acctHashList.size}") + registerEndpointFcm(acctHashList) + } + else -> { + log.i("registerEndpoint dist=${prefDevice.pushDistributor}, acctHashList=${acctHashList.size}") + registerEndpointUnifiedPush(acctHashList) + } } + when { json.isNullOrEmpty() -> log.i("no information of appServerHash.") @@ -355,8 +369,7 @@ class PushRepo( null } else -> { - log.i("endpointUpsert up ") - apiPushAppServer.endpointUpsert( + apiAppServer.endpointUpsert( upUrl = upEndpoint, fcmToken = null, acctHashList = acctHashList @@ -371,8 +384,7 @@ class PushRepo( null } else -> { - log.i("endpointUpsert fcm ") - apiPushAppServer.endpointUpsert( + apiAppServer.endpointUpsert( upUrl = null, fcmToken = fcmToken, acctHashList = acctHashList @@ -436,7 +448,7 @@ class PushRepo( * * - 実際のアプリでは解読できたものだけを保存したいが、これは試験アプリなので… */ - suspend fun reDecode(pm: PushMessage) { + suspend fun reprocess(pm: PushMessage) { withContext(AppDispatchers.IO) { updateMessage(pm.id, allowDupilicateNotification = true) } @@ -463,7 +475,7 @@ class PushRepo( // アプリサーバから読み直す if (map["b"] == null) { map.string("l")?.let { largeObjectId -> - apiPushAppServer.getLargeObject(largeObjectId) + apiAppServer.getLargeObject(largeObjectId) ?.let { map = it.decodeBinPack() as? BinPackMap ?: error("binPack decode failed.") @@ -479,14 +491,14 @@ class PushRepo( val status = daoStatus.findByAcctHash(acctHash) ?: error("missing status for acctHash $acctHash") - val acct = status.acct.notEmpty() + val acct = status.acct.takeIf { it.isValidFull } ?: error("empty acct.") - val account = daoSavedAccount.loadAccountByAcct(Acct.parse(acct)) - ?: error("missing account for acct ${status.acct}") - pm.loginAcct = status.acct + val account = daoSavedAccount.loadAccountByAcct(acct) + ?: error("missing account for acct ${status.acct}") + decodeMessageContent(status, pm, map) val messageJson = pm.messageJson @@ -496,7 +508,7 @@ class PushRepo( // メッセージに含まれるappServerHashを指定してendpoint登録を削除する // するとアプリサーバはSNSサーバに対してgoneを返すようになり掃除が適切に行われるはず map.string("c").notEmpty()?.let { - val count = apiPushAppServer.endpointRemove(hashId = it).int("count") + val count = apiAppServer.endpointRemove(hashId = it).int("count") log.w("endpointRemove $count hashId=$it") } @@ -504,29 +516,37 @@ class PushRepo( } // Mastodonはなぜかアクセストークンが書いてあるので危険… - val censored = messageJson.toString() + val messageJsonFiltered = messageJson.toString() .replace( """"access_token":"[^"]+"""".toRegex(), """"access_token":"***"""" ) - log.i("${status.acct} $censored") + log.i("${status.acct} $messageJsonFiltered") + + // ミュート用データを時々読む + TootStatus.updateMuteData() // messageJsonを解釈して通知に出す内容を決める - TootStatus.updateMuteData() pushBase(account).formatPushMessage(account, pm) val notificationId = pm.notificationId if (notificationId.isNullOrEmpty()) { - error("can't show notification. missing notificationId.") + log.w("can't show notification. missing notificationId.") + return + } + + if (!account.canNotificationShowing(pm.notificationType)) { + log.w("notificationType ${pm.notificationType} is disabled.") + return } if (!allowDupilicateNotification && daoNotificationShown.duplicateOrPut(acct, notificationId) ) { - error("can't show notification. it's duplicate. $acct $notificationId") + log.w("can't show notification. it's duplicate. $acct $notificationId") + return } - // 解読できた(例外が出なかった)なら通知を出す showPushNotification(pm, account, notificationId) } catch (ex: Throwable) { log.e(ex, "updateMessage failed.") @@ -620,8 +640,8 @@ class PushRepo( account: SavedAccount, notificationId: String, ) { - if (ncPushMessage.isDissabled(context)) { - log.w("ncPushMessage isDissabled.") + if (ncPushMessage.isDisabled(context)) { + log.w("ncPushMessage isDisabled.") return } @@ -672,14 +692,18 @@ class PushRepo( PendingIntent.FLAG_IMMUTABLE ) - // val iTap = intentActMessage(pm.messageDbId) + // val iTap = intentActMessage(pm.messageDbId) // val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE) ncPushMessage.notify(context, urlDelete) { - color = ContextCompat.getColor(context, iconAndColor.colorRes) + color = pm.iconColor().colorRes.notZero() + ?.let { ContextCompat.getColor(context, it) } + ?: account.notificationAccentColor.notZero() + ?: ContextCompat.getColor(context, R.color.colorOsNotificationAccent) + setSmallIcon(iconSmall) iconBitmapLarge?.let { setLargeIcon(it) } - setContentTitle(pm.loginAcct) + setContentTitle(pm.loginAcct?.pretty) setContentText(pm.text) setWhen(pm.timestamp) setContentIntent(piTap) @@ -688,6 +712,8 @@ class PushRepo( pm.textExpand.notEmpty()?.let { setStyle(NotificationCompat.BigTextStyle().bigText(it)) } + + setGroup(context.packageName + ":" + account.acct.ascii) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/push/PushWorker.kt b/app/src/main/java/jp/juggler/subwaytooter/push/PushWorker.kt index 6d4d2311..86302c58 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushWorker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushWorker.kt @@ -1,7 +1,10 @@ package jp.juggler.subwaytooter.push +import android.app.PendingIntent import android.content.Context +import android.content.Intent import androidx.work.* +import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.notification.NotificationChannels import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.data.notEmpty @@ -15,6 +18,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : companion object { private val log = LogCategory("PushWorker") + private val ncPushWorker = NotificationChannels.PushWorker + const val KEY_ACTION = "action" const val KEY_ENDPOINT = "endpoint" const val KEY_MESSAGE_ID = "messageId" @@ -31,8 +36,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : fun enqueueUpEndpoint(context: Context, endpoint: String) { workDataOf( - PushWorker.KEY_ACTION to PushWorker.ACTION_UP_ENDPOINT, - PushWorker.KEY_ENDPOINT to endpoint, + KEY_ACTION to ACTION_UP_ENDPOINT, + KEY_ENDPOINT to endpoint, ).launchPushWorker(context) } @@ -45,8 +50,8 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : fun enqueuePushMessage(context: Context, messageId: Long) { workDataOf( - PushWorker.KEY_ACTION to PushWorker.ACTION_MESSAGE, - PushWorker.KEY_MESSAGE_ID to messageId, + KEY_ACTION to ACTION_MESSAGE, + KEY_MESSAGE_ID to messageId, ).launchPushWorker(context) } @@ -68,9 +73,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : } override suspend fun doWork(): Result = try { - NotificationChannels.PushWorker.createForegroundInfo( - applicationContext, - )?.let{setForegroundAsync(it)} + createForegroundInfo()?.let { setForegroundAsync(it) } withContext(AppDispatchers.IO) { val pushRepo = applicationContext.pushRepo when (val action = inputData.getString(KEY_ACTION)) { @@ -80,7 +83,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : val endpoint = inputData.getString(KEY_ENDPOINT) ?.notEmpty() ?: error("missing endpoint.") pushRepo.newUpEndpoint(endpoint) - }finally{ + } finally { timeEndUpEndpoint.set(System.currentTimeMillis()) } } @@ -89,7 +92,7 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : try { val keepAliveMode = inputData.getBoolean(KEY_KEEP_ALIVE_MODE, false) pushRepo.registerEndpoint(keepAliveMode) - }finally{ + } finally { timeEndRegisterEndpoint.set(System.currentTimeMillis()) } } @@ -106,4 +109,32 @@ class PushWorker(appContext: Context, workerParams: WorkerParameters) : log.e(ex, "doWork failed.") Result.failure() } + + /** + * 時々OSに呼ばれる + * Android 11 moto g31 で発生 + */ + override suspend fun getForegroundInfo(): ForegroundInfo { + return createForegroundInfo(force = true)!! + } + + private fun createForegroundInfo(force: Boolean = false): ForegroundInfo? { + val context = applicationContext + + // 通知タップ時のPendingIntent + val iTap = Intent(context, ActMain::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + val piTap = PendingIntent.getActivity( + context, + ncPushWorker.pircTap, + iTap, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + return ncPushWorker.createForegroundInfo( + context, + piTap = piTap, + force = force, + ) + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt index 05670bdf..50db6f18 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/NetworkEmojiSpan.kt @@ -4,17 +4,23 @@ import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect import android.graphics.RectF +import android.graphics.drawable.Drawable import android.text.style.ReplacementSpan +import androidx.annotation.DrawableRes import androidx.annotation.IntRange +import androidx.core.content.ContextCompat import jp.juggler.apng.ApngFrames import jp.juggler.subwaytooter.App1 +import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.pref.PrefB +import jp.juggler.subwaytooter.pref.lazyContext import jp.juggler.util.log.LogCategory import java.lang.ref.WeakReference class NetworkEmojiSpan internal constructor( private val url: String, private val scale: Float = 1f, + @DrawableRes private val errorDrawableId: Int = R.drawable.outline_broken_image_24, ) : ReplacementSpan(), AnimatableSpan { companion object { @@ -25,20 +31,19 @@ class NetworkEmojiSpan internal constructor( private const val descentRatio = 0.211f } - private val mPaint = Paint() + private val mPaint = Paint().apply { isFilterBitmap = true } private val rectSrc = Rect() private val rectDst = RectF() // フレーム探索結果を格納する構造体を確保しておく private val mFrameFindResult = ApngFrames.FindFrameResult() - init { - mPaint.isFilterBitmap = true - } - private var invalidateCallback: AnimatableSpanInvalidator? = null + private var refDrawTarget: WeakReference? = null + private var errorDrawableCache: Drawable? = null + override fun setInvalidateCallback( drawTargetTag: Any, invalidateCallback: AnimatableSpanInvalidator, @@ -77,16 +82,26 @@ class NetworkEmojiSpan internal constructor( bottom: Int, textPaint: Paint, ) { + if (drawFrame(canvas, x, baseline, textPaint)) return + drawError(canvas, x, baseline, textPaint) + } + + private fun drawFrame( + canvas: Canvas, + x: Float, + baseline: Int, + textPaint: Paint, + ): Boolean { val invalidateCallback = this.invalidateCallback if (invalidateCallback == null) { log.e("draw: invalidate_callback is null.") - return + return false } // APNGデータの取得 val frames = App1.custom_emoji_cache.getFrames(refDrawTarget, url) { invalidateCallback.delayInvalidate(0L) - } ?: return + } ?: return false val t = when { PrefB.bpDisableEmojiAnimation.value -> 0L @@ -99,13 +114,13 @@ class NetworkEmojiSpan internal constructor( val b = mFrameFindResult.bitmap if (b == null || b.isRecycled) { log.e("draw: bitmap is null or recycled.") - return + return false } val srcWidth = b.width val srcHeight = b.height if (srcWidth < 1 || srcHeight < 1) { log.e("draw: bitmap size is too small.") - return + return false } rectSrc.set(0, 0, srcWidth, srcHeight) @@ -155,5 +170,67 @@ class NetworkEmojiSpan internal constructor( if (delay != Long.MAX_VALUE && !PrefB.bpDisableEmojiAnimation.value) { invalidateCallback.delayInvalidate(delay) } + return true + } + + private fun drawError( + canvas: Canvas, + x: Float, + baseline: Int, + textPaint: Paint, + ) { + val drawable = errorDrawableCache + ?: ContextCompat.getDrawable(lazyContext, errorDrawableId) + ?.also { errorDrawableCache = it } + + drawable ?: return + val srcWidth = drawable.intrinsicWidth + val srcHeight = drawable.intrinsicHeight + + // 絵文字の正方形のサイズ + val dstSize = textPaint.textSize * scaleRatio * scale + + // ベースラインから上下方向にずらすオフセット + val cDescent = dstSize * descentRatio + val transY = baseline - dstSize + cDescent + + // 絵文字のアスペクト比から描画範囲の幅と高さを決める + val dstWidth: Float + val dstHeight: Float + val aspectSrc = srcWidth.toFloat() / srcHeight.toFloat() + if (aspectSrc >= 1f) { + dstWidth = dstSize + dstHeight = dstSize / aspectSrc + } else { + dstHeight = dstSize + dstWidth = dstSize * aspectSrc + } + val dstX = (dstSize - dstWidth) / 2f + val dstY = (dstSize - dstHeight) / 2f + // rectDst.set(dstX, dstY, dstX + dstWidth, dstY + dstHeight) + canvas.save() + try { + canvas.translate(x, transY) + drawable.setBounds( + dstX.toInt(), + dstY.toInt(), + (dstX + dstWidth).toInt(), + (dstY + dstHeight).toInt() + ) + drawable.draw(canvas) + } catch (ex: Throwable) { + log.w(ex, "drawBitmap failed.") + + // 10月6日 18:18(アプリのバージョン: 378) Sony Xperia X Compact(F5321), Android 8.0 + // 10月6日 11:35(アプリのバージョン: 380) Samsung Galaxy S7 Edge(hero2qltetmo), Android 8.0 + // 10月2日 21:56(アプリのバージョン: 376) Google Pixel 3(blueline), Android 9 + // java.lang.RuntimeException: + // at android.graphics.BaseCanvas.throwIfCannotDraw (BaseCanvas.java:55) + // at android.view.DisplayListCanvas.throwIfCannotDraw (DisplayListCanvas.java:226) + // at android.view.RecordingCanvas.drawBitmap (RecordingCanvas.java:123) + // at jp.juggler.subwaytooter.span.NetworkEmojiSpan.draw (NetworkEmojiSpan.kt:137) + } finally { + canvas.restore() + } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt index a2c966db..2c0b9be5 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/streaming/StreamConnection.kt @@ -5,6 +5,7 @@ import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.entity.* +import jp.juggler.subwaytooter.api.entity.MisskeyNoteUpdate.Companion.misskeyNoteUpdate import jp.juggler.subwaytooter.column.onStatusRemoved import jp.juggler.subwaytooter.column.reloadFilter import jp.juggler.subwaytooter.column.replaceStatus @@ -201,13 +202,7 @@ class StreamConnection( log.e("$name handleMisskeyMessage: noteUpdated body is null") return } - fireNoteUpdated( - MisskeyNoteUpdate( - acctGroup.account.apDomain, - acctGroup.account.apiHost, - body - ), channelId - ) + fireNoteUpdated(misskeyNoteUpdate(body), channelId) } "notification" -> { diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/AccountNotificationStatus.kt b/app/src/main/java/jp/juggler/subwaytooter/table/AccountNotificationStatus.kt index 52854557..63c87cdb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/AccountNotificationStatus.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/AccountNotificationStatus.kt @@ -12,7 +12,7 @@ class AccountNotificationStatus( // DB上のID var id: Long = 0L, // 該当ユーザのacct - var acct: String = "", + var acct: Acct = Acct.UNKNOWN, // acctのハッシュ値 var acctHash: String = "", // アプリサーバから受け取ったハッシュ @@ -100,7 +100,7 @@ class AccountNotificationStatus( cursor ?: error("cursor is null!") AccountNotificationStatus( id = cursor.getLong(idxId), - acct = cursor.getString(idxAcct), + acct = Acct.parse(cursor.getString(idxAcct)), acctHash = cursor.getString(idxAcctHash), appServerHash = cursor.getStringOrNull(idxAppServerHash), pushKeyPrivate = cursor.getBlobOrNull(idxPushKeyPrivate), @@ -119,7 +119,7 @@ class AccountNotificationStatus( // ID以外のカラムをContentValuesに変換する fun toContentValues() = ContentValues().apply { - put(COL_ACCT, acct) + put(COL_ACCT, acct.ascii) put(COL_ACCT_HASH, acctHash) put(COL_APP_SERVER_HASH, appServerHash) put(COL_PUSH_KEY_PRIVATE, pushKeyPrivate) @@ -154,7 +154,7 @@ class AccountNotificationStatus( private fun newInstance(acct: Acct) = AccountNotificationStatus( - acct = acct.ascii, + acct = acct, acctHash = acct.ascii.encodeUTF8().digestSHA256().encodeBase64Url() ) diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationShown.kt b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationShown.kt index 4e0a78bb..a9cac5b4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationShown.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationShown.kt @@ -4,6 +4,7 @@ import android.content.ContentValues import android.database.sqlite.SQLiteDatabase import android.provider.BaseColumns import jp.juggler.subwaytooter.api.entity.Acct +import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.util.data.MetaColumns import jp.juggler.util.data.TableCompanion import jp.juggler.util.data.replaceTo @@ -15,7 +16,6 @@ class NotificationShown( var acct: String = "", var notificationId: String = "", var timeCreate: Long = System.currentTimeMillis(), - var timeDismiss: Long = 0L, ) { companion object : TableCompanion { private val log = LogCategory("NotificationShown") @@ -24,13 +24,11 @@ class NotificationShown( private const val COL_ACCT = "a" private const val COL_NOTIFICATION_ID = "ni" private const val COL_TIME_CREATE = "tc" - private const val COL_TIME_DISMISS = "td" private val columnList = MetaColumns(table, initialVersion = 65).apply { column(0, COL_ID, MetaColumns.TS_INT_PRIMARY_KEY_NOT_NULL) column(0, COL_ACCT, MetaColumns.TS_EMPTY_NOT_NULL) column(0, COL_NOTIFICATION_ID, MetaColumns.TS_EMPTY_NOT_NULL) column(0, COL_TIME_CREATE, MetaColumns.TS_ZERO_NOT_NULL) - column(0, COL_TIME_DISMISS, MetaColumns.TS_ZERO_NOT_NULL) createExtra = { arrayOf( "create unique index if not exists ${table}_a on $table($COL_ACCT,$COL_NOTIFICATION_ID)", @@ -122,24 +120,29 @@ class NotificationShown( } } - fun cleayByAcct(acct: String) { + fun cleayByAcct(acct: Acct) { db.execSQL( "delete from $table where $COL_ACCT=?", arrayOf(acct) ) } - fun duplicateOrPut(acct: String, notificationId: String): Boolean { + fun duplicateOrPut(acct: Acct, notificationId: String): Boolean { try { + // 有効なIDがない場合は重複排除しない + when (notificationId) { + "", EntityId.DEFAULT.toString() -> return false + } + db.rawQuery( "select $COL_ID from $table where $COL_ACCT=? and $COL_NOTIFICATION_ID=? limit 1", - arrayOf(acct, notificationId) + arrayOf(acct.ascii, notificationId) )?.use { if (it.count > 0) return true } ContentValues().apply { put(COL_TIME_CREATE, System.currentTimeMillis()) - put(COL_ACCT, acct) + put(COL_ACCT, acct.ascii) put(COL_NOTIFICATION_ID, notificationId) }.replaceTo(db, table) } catch (ex: Throwable) { @@ -148,5 +151,23 @@ class NotificationShown( return false } + fun isDuplicate(acct: Acct, notificationId: String): Boolean { + try { + // 有効なIDがない場合は重複排除しない + when (notificationId) { + "", EntityId.DEFAULT.toString() -> return false + } + + db.rawQuery( + "select $COL_ID from $table where $COL_ACCT=? and $COL_NOTIFICATION_ID=? limit 1", + arrayOf(acct.ascii, notificationId) + )?.use { + if (it.count > 0) return true + } + } catch (ex: Throwable) { + log.e(ex, "isDuplicate failed.") + } + return false + } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/PushMessage.kt b/app/src/main/java/jp/juggler/subwaytooter/table/PushMessage.kt index aeb6776c..2e7410ce 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/PushMessage.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/PushMessage.kt @@ -16,7 +16,7 @@ data class PushMessage( // DBの主ID var id: Long = 0L, // 通知を受け取るアカウントのacct。通知のタイトルでもある - var loginAcct: String? = null, + var loginAcct: Acct? = null, // 通知情報に含まれるタイムスタンプ var timestamp: Long = System.currentTimeMillis(), // 通知を受信/保存した時刻 @@ -151,7 +151,7 @@ data class PushMessage( fun readRow(cursor: Cursor) = PushMessage( id = cursor.getLong(idxId), - loginAcct = cursor.getStringOrNull(idxLoginAcct), + loginAcct = cursor.getStringOrNull(idxLoginAcct)?.let{Acct.parse(it)}, timestamp = cursor.getLong(idxTimestamp), timeSave = cursor.getLong(idxTimeSave), timeDismiss = cursor.getLong(idxTimeDismiss), @@ -183,7 +183,7 @@ data class PushMessage( // ID以外のカラムをContentValuesに変換する fun toContentValues() = ContentValues().apply { - put(COL_LOGIN_ACCT, loginAcct) + put(COL_LOGIN_ACCT, loginAcct?.ascii) put(COL_TIMESTAMP, timestamp) put(COL_TIME_SAVE, timeSave) put(COL_TIME_DISMISS, timeDismiss) @@ -253,6 +253,18 @@ data class PushMessage( db.queryAll(TABLE, "$COL_TIME_SAVE desc") ?.use { ColIdx(it).readAll(it) } ?: emptyList() + + fun deleteAccount(acct: Acct) { + try { + db.execSQL( + "delete from $TABLE where $COL_LOGIN_ACCT=?", + arrayOf(acct.ascii) + ) + fireDataChanged() + } catch (ex: Throwable) { + log.e(ex, "sweep failed.") + } + } } override fun hashCode() = if (id == 0L) super.hashCode() else id.hashCode() diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt index 55577db7..cc35bd1f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt @@ -115,9 +115,14 @@ class SavedAccount( @JsonPropBoolean("notificationPullEnable", false) var notificationPullEnable by jsonDelegates.boolean + @JsonPropInt("notificationAccentColor", 0) + var notificationAccentColor by jsonDelegates.int + init { log.i("ctor acctArg $acctArg") + // acctArg はMastodonの生のやつで、ドメイン部分がない場合がある + // Acct.parse はHost部分がnullのacctになるかもしれない val tmpAcct = Acct.parse(acctArg) this.username = tmpAcct.username if (username.isEmpty()) error("missing username in acct") @@ -128,6 +133,7 @@ class SavedAccount( this.apiHost = tmpApiHost ?: tmpApDomain ?: tmpAcct.host ?: error("missing apiHost") this.apDomain = tmpApDomain ?: tmpApiHost ?: tmpAcct.host ?: error("missing apDomain") + // Full Acct this.acct = tmpAcct.followHost(apDomain) } @@ -590,15 +596,24 @@ class SavedAccount( fun loadAccountList() = ArrayList().also { result -> try { - db.query( - table, - null, - null, - null, - null, - null, - null - ).use { cursor -> + db.rawQuery("select * from $table", emptyArray()).use { cursor -> + while (cursor.moveToNext()) { + parse(lazyContext, cursor)?.let { result.add(it) } + } + } + } catch (ex: Throwable) { + log.e(ex, "loadAccountList failed.") + lazyContext.showToast( + true, + ex.withCaption("(SubwayTooter) broken in-app database?") + ) + } + } + + fun loadRealAccounts() = + ArrayList().also { result -> + try { + db.rawQuery("select * from $table where $COL_USER not like '?%'", emptyArray()).use { cursor -> while (cursor.moveToNext()) { parse(lazyContext, cursor)?.let { result.add(it) } } @@ -892,7 +907,8 @@ class SavedAccount( TootNotification.TYPE_STATUS_REFERENCE -> notificationStatusReference - else -> false + // 未知の通知はオフらない + else -> true } fun getResizeConfig() = diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt index 41004de8..195aec30 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.kt @@ -231,7 +231,7 @@ class CustomEmojiCache( data = try { App1.getHttpCached(request.url) } catch (ex: Throwable) { - log.w(ex, "get failed. url=${request.url}") + log.w( "get failed. url=${request.url}") null } te = elapsedTime diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt index e7bc057f..3780c961 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiLister.kt @@ -224,15 +224,7 @@ class CustomEmojiLister( builder.post(JsonObject().toRequestBody()) }?.decodeJsonObject() ?.jsonArray("emojis") - ?.let { emojis12 -> - parseList(emojis12) { - CustomEmoji.decodeMisskey( - accessInfo.apDomain, - accessInfo.apiHost, - it - ) - } - } + ?.let { parseList(it, CustomEmoji::decodeMisskey) } // v13のemojisを読む suspend fun misskeyEmojis13(): List? = @@ -247,11 +239,7 @@ class CustomEmojiLister( ?.jsonArray("emojis") ?.let { emojis13 -> parseList(emojis13) { - CustomEmoji.decodeMisskey13( - accessInfo.apDomain, - accessInfo.apiHost, - it - ) + CustomEmoji.decodeMisskey13(accessInfo.apiHost, it) } } @@ -261,13 +249,7 @@ class CustomEmojiLister( "https://$cacheKey/api/v1/custom_emojis", accessInfo = accessInfo )?.let { data -> - parseList(data.decodeJsonArray()) { - CustomEmoji.decode( - accessInfo.apDomain, - accessInfo.apiHost, - it - ) - } + parseList(data.decodeJsonArray(), CustomEmoji::decodeMastodon) } val list = when { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.kt b/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.kt index d85c0c4e..07e98fea 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/DecodeOptions.kt @@ -20,8 +20,8 @@ class DecodeOptions( var decodeEmoji: Boolean = false, var attachmentList: ArrayList? = null, var linkTag: Any? = null, - var emojiMapCustom: HashMap? = null, - var emojiMapProfile: HashMap? = null, + var emojiMapCustom: Map? = null, + var emojiMapProfile: Map? = null, var highlightTrie: WordTrieTree? = null, var unwrapEmojiImageTag: Boolean = false, var enlargeCustomEmoji: Float = 1f, diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt b/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt index 70cb5005..cdce00d8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/EmojiDecoder.kt @@ -469,6 +469,56 @@ object EmojiDecoder { val useEmojioneShortcode = PrefB.bpEmojioneShortcode.value val disableEmojiAnimation = PrefB.bpDisableEmojiAnimation.value + // カスタム絵文字のアニメーション切り替え + fun CustomEmoji.customEmojiToUrl(): String = when { + disableEmojiAnimation && staticUrl?.isNotEmpty() == true -> staticUrl + else -> this.url + } + + fun findEmojiMisskey13(name: String): String? { + val cols = name.split("@", limit = 2) + val apiHostAscii = options.linkHelper?.apiHost?.ascii + + // @以降にあるホスト名か、投稿者のホスト名か、閲覧先サーバのホスト名 + val userHost = cols.elementAtOrNull(1) + ?: options.authorDomain?.apiHost?.ascii + ?: apiHostAscii + log.i( + "decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${ + cols.elementAtOrNull(1) + } apiHostAscii=$apiHostAscii, userHost=$userHost" + ) + + when { + // 絵文字プロクシを利用できない + apiHostAscii == null -> { + log.w("decodeEmoji Misskey13 missing apiHostAscii") + } + userHost != null && userHost != "." && userHost != apiHostAscii -> { + // 投稿者のホスト名を使う + return "https://$apiHostAscii/emoji/${ + cols.elementAtOrNull(0) + }@$userHost.webp" + } + else -> { + // 存在確認せずに絵文字プロキシのURLを返す + // 閲覧先サーバの絵文字を探す + App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name) + ?.let { return it.customEmojiToUrl() } + } + } + return null + } + fun findCustomEmojiUrl(name: String): String? { + emojiMapCustom?.get(name)?.customEmojiToUrl() + ?.let{ return it} + val misskeyVersion = options.linkHelper?.misskeyVersion ?: 0 + return if (misskeyVersion >= 13) { + findEmojiMisskey13(name = name) + } else { + null + } + } splitShortCode(s, callback = object : ShortCodeSplitterCallback { override fun onString(part: String) { builder.addUnicodeString(part) @@ -487,53 +537,7 @@ object EmojiDecoder { } } - // カスタム絵文字 - fun CustomEmoji.customEmojiToUrl(): String = when { - disableEmojiAnimation && staticUrl?.isNotEmpty() == true -> - this.staticUrl - else -> - this.url - } - - fun findCustomEmojiUrl(): String? { - val misskeyVersion = options.linkHelper?.misskeyVersion ?: 0 - if (misskeyVersion >= 13) { - val cols = name.split("@", limit = 2) - val apiHostAscii = options.linkHelper?.apiHost?.ascii - - // @以降にあるホスト名か、投稿者のホスト名か、閲覧先サーバのホスト名 - val userHost = cols.elementAtOrNull(1) - ?: options.authorDomain?.apiHost?.ascii - ?: apiHostAscii - - log.i( - "decodeEmoji Misskey13 c0=${cols.elementAtOrNull(0)} c1=${ - cols.elementAtOrNull(1) - } apiHostAscii=$apiHostAscii, userHost=$userHost" - ) - - when { - // 絵文字プロクシを利用できない - apiHostAscii == null -> { - log.w("decodeEmoji Misskey13 missing apiHostAscii") - } - userHost != null && userHost != "." && userHost != apiHostAscii -> { - // 投稿者のホスト名を使う - return "https://$apiHostAscii/emoji/${ - cols.elementAtOrNull(0) - }@$userHost.webp" - } - else -> { - // 閲覧先サーバの絵文字を探す - App1.custom_emoji_lister.getCachedEmoji(apiHostAscii, name) - ?.let { return it.customEmojiToUrl() } - } - } - } - return emojiMapCustom?.get(name)?.customEmojiToUrl() - } - - val url = findCustomEmojiUrl() + val url = findCustomEmojiUrl(name) if (url != null) { builder.addNetworkEmojiSpan(part, url) return diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt b/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt index 297acb5f..cb6cd97a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/view/MyNetworkImageView.kt @@ -15,23 +15,27 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.LazyHeaders +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoader.LoadData import com.bumptech.glide.load.resource.gif.GifDrawable import com.bumptech.glide.load.resource.gif.MyGifDrawable +import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.target.ImageViewTarget import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.util.data.clip +import jp.juggler.util.data.notEmpty import jp.juggler.util.log.LogCategory +import java.nio.ByteBuffer class MyNetworkImageView : AppCompatImageView { - companion object { - internal val log = LogCategory("MyNetworkImageView") - } - // ロード中などに表示するDrawableのリソースID private var mDefaultImage: Drawable? = null @@ -43,7 +47,7 @@ class MyNetworkImageView : AppCompatImageView { // 表示したい画像のURL private var mUrl: String? = null - private var mMayGif: Boolean = false + private var mMayAnime: Boolean = false // 非同期処理のキャンセル private var mTarget: Target<*>? = null @@ -76,20 +80,19 @@ class MyNetworkImageView : AppCompatImageView { fun setImageUrl( r: Float, url: String?, - gifUrlArg: String? = null, + animeUrl: String? = null, ) { - mCornerRadius = r - - val gifUrl = if (PrefB.bpEnableGifAnimation.value) gifUrlArg else null - - if (gifUrl?.isNotEmpty() == true) { - mUrl = gifUrl - mMayGif = true - } else { - mUrl = url - mMayGif = false + if (PrefB.bpEnableGifAnimation.value) { + animeUrl?.notEmpty()?.let { + mUrl = it + mMayAnime = true + loadImageIfNecessary() + return + } } + mUrl = url + mMayAnime = false loadImageIfNecessary() } @@ -178,13 +181,15 @@ class MyNetworkImageView : AppCompatImageView { val glideUrl = GlideUrl(url, glideHeaders) - mTarget = if (mMayGif) { + mTarget = if (mMayAnime) { getGlide() ?.load(glideUrl) + ?.listener(listener) ?.into(MyTargetGif(url)) } else { getGlide() ?.load(glideUrl) + ?.listener(listener) ?.into(MyTarget(url)) } } catch (ex: Throwable) { @@ -563,4 +568,61 @@ class MyNetworkImageView : AppCompatImageView { } } } + + companion object { + private val log = LogCategory("MyNetworkImageView") + private val listener = MyRequestListener() + private val misskey13ModelLoader = Misskey13ModelLoader() + } + + class MyRequestListener : RequestListener { + override fun onResourceReady( + resource: Drawable, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean, + ): Boolean { + return false // Allow calling onResourceReady on the Target. + } + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean { + e?.let { + log.e(it, "onLoadFailed") + it.rootCauses?.forEach { cause -> + val message = cause?.message + when { + cause == null -> Unit + message?.contains("setDataSource failed: status") == true || + message?.contains("etDataSourceCallback failed: status") == true + -> log.w(message) + else -> log.e(cause, "caused by") + } + } + } + return false // Allow calling onLoadFailed on the Target. + } + } + + class Misskey13ModelLoader : ModelLoader { + override fun handles(model: String): Boolean { + return model.startsWith("http") && + model.contains(".webp?") + } + + override fun buildLoadData( + model: String, + width: Int, + height: Int, + options: Options, + ): LoadData? { + TODO("Not yet implemented") + } + } } + diff --git a/app/src/main/res/drawable/outline_broken_image_24.xml b/app/src/main/res/drawable/outline_broken_image_24.xml new file mode 100644 index 00000000..fdb9cb3e --- /dev/null +++ b/app/src/main/res/drawable/outline_broken_image_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/act_account_setting.xml b/app/src/main/res/layout/act_account_setting.xml index 0903bdec..c015d04b 100644 --- a/app/src/main/res/layout/act_account_setting.xml +++ b/app/src/main/res/layout/act_account_setting.xml @@ -594,6 +594,42 @@ android:layout_weight="1" /> + + + + + + + + + + + + + android:text="@string/pull_notification_use" /> diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 41818286..ac216baf 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1237,4 +1237,6 @@ 定期的なプル通知チェックを使う 現在のプッシュ購読を更新しています… 手動Manually update + 通知のプッシュ配送サービスが選択されていません + 通知のアクセント色 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f99f6c6c..af6485ac 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -166,7 +166,8 @@ #f5f233 #39e3d5 #ff3dbb - #f56a33 + #f56a33 + #ff0000 #33f597 #9433f5 #ae1aed diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6c75411d..c8b5a983 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1253,4 +1253,6 @@ Fedibird capacities Updating current push subscription… Manually update + Notification push distributor not selected. + Notification accent color diff --git a/base/build.gradle b/base/build.gradle index 4c0248d8..cbee285a 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -101,6 +101,7 @@ dependencies { // api "io.insert-koin:koin-androidx-navigation:$koinVersion" // api "io.insert-koin:koin-androidx-compose:$koinVersion" + api "com.github.zjupure:webpdecoder:2.3.$glideVersion" api "com.github.bumptech.glide:glide:$glideVersion" api "com.github.bumptech.glide:annotations:$glideVersion" api("com.github.bumptech.glide:okhttp3-integration:$glideVersion") {