From 03352bb1804908c0ea0d7922b80f025ee8cbc332 Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 9 Feb 2023 09:05:59 +0900 Subject: [PATCH] fix accesstoken update issue --- .../subwaytooter/actmain/ActMainActions.kt | 11 +- .../subwaytooter/actmain/ActMainIntent.kt | 57 ++-- .../subwaytooter/api/auth/AuthMastodon.kt | 5 + .../appsetting/AppDataExporter.kt | 2 +- .../jp/juggler/subwaytooter/column/Column.kt | 2 +- .../columnviewholder/ViewHolderHeaderBase.kt | 18 +- .../ViewHolderHeaderInstance.kt | 3 + .../jp/juggler/subwaytooter/push/PushBase.kt | 3 +- .../juggler/subwaytooter/push/PushMastodon.kt | 130 ++++---- .../juggler/subwaytooter/push/PushMisskey.kt | 26 +- .../jp/juggler/subwaytooter/push/PushRepo.kt | 298 ++++++++++-------- .../subwaytooter/table/SavedAccount.kt | 33 +- .../main/res/layout/lv_header_instance.xml | 159 ++++------ app/src/main/res/values-ja/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 15 files changed, 396 insertions(+), 353 deletions(-) diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainActions.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainActions.kt index 2ec22ef5..463ded45 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainActions.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainActions.kt @@ -32,6 +32,7 @@ import jp.juggler.util.data.notEmpty import jp.juggler.util.log.LogCategory import jp.juggler.util.log.showToast import jp.juggler.util.ui.dismissSafe +import kotlinx.coroutines.delay import kotlinx.coroutines.suspendCancellableCoroutine import java.util.concurrent.TimeUnit @@ -256,12 +257,16 @@ fun ActMain.launchDialogs() { } } -fun ActMain.afterNotificationGranted() { +suspend fun ActMain.afterNotificationGranted() { sideMenuAdapter.filterListItems() // Workの掃除 WorkManager.getInstance(applicationContext).pruneWork() - // 定期的にendpointを再登録したい - PushWorker.enqueueRegisterEndpoint(applicationContext, keepAliveMode = true) + // 認証やアクセストークン更新から戻ってきた時に処理を重ねたくない + delay(2000L) + if (!accountVerifyMutex.isLocked) { + // 定期的にendpointを再登録したい + PushWorker.enqueueRegisterEndpoint(applicationContext, keepAliveMode = true) + } } 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 3d96956b..aa3b1df2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainIntent.kt @@ -31,7 +31,6 @@ import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll import jp.juggler.subwaytooter.notification.recycleClickedNotification import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.prefDevice -import jp.juggler.subwaytooter.push.PushWorker import jp.juggler.subwaytooter.push.fcmHandler import jp.juggler.subwaytooter.push.pushRepo import jp.juggler.subwaytooter.table.SavedAccount @@ -44,7 +43,8 @@ import jp.juggler.util.data.groupEx import jp.juggler.util.log.LogCategory import jp.juggler.util.log.showToast import jp.juggler.util.queryIntentActivitiesCompat -import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.unifiedpush.android.connector.UnifiedPush @@ -229,32 +229,36 @@ private fun ActMain.handleOAuth2Callback(uri: Uri) { } } +val accountVerifyMutex = Mutex() + /** * アカウントを確認した後に呼ばれる * @return 何かデータを更新したら真 */ -fun ActMain.afterAccountVerify(auth2Result: Auth2Result): Boolean = auth2Result.run { +suspend fun ActMain.afterAccountVerify(auth2Result: Auth2Result): Boolean = auth2Result.run { + accountVerifyMutex.withLock { + // ユーザ情報中のacctはfull acct ではないので、組み立てる + val newAcct = Acct.parse(tootAccount.username, apDomain) - // ユーザ情報中のacctはfull acct ではないので、組み立てる - val newAcct = Acct.parse(tootAccount.username, apDomain) + // full acctだよな? + """\A[^@]+@[^@]+\z""".toRegex().find(newAcct.ascii) + ?: error("afterAccountAdd: incorrect userAcct. ${newAcct.ascii}") - // full acctだよな? - """\A[^@]+@[^@]+\z""".toRegex().find(newAcct.ascii) - ?: error("afterAccountAdd: incorrect userAcct. ${newAcct.ascii}") - - // 「アカウント追加のハズが既存アカウントで認証していた」 - // 「アクセストークン更新のハズが別アカウントで認証していた」 - // などを防止するため、full acctでアプリ内DBを検索 - when (val sa = daoSavedAccount.loadAccountByAcct(newAcct)) { - null -> afterAccountAdd(newAcct, auth2Result) - else -> afterAccessTokenUpdate(auth2Result, sa) + // 「アカウント追加のハズが既存アカウントで認証していた」 + // 「アクセストークン更新のハズが別アカウントで認証していた」 + // などを防止するため、full acctでアプリ内DBを検索 + when (val sa = daoSavedAccount.loadAccountByAcct(newAcct)) { + null -> afterAccountAdd(newAcct, auth2Result) + else -> afterAccessTokenUpdate(auth2Result, sa) + } } } -private fun ActMain.afterAccessTokenUpdate( +private suspend fun ActMain.afterAccessTokenUpdate( auth2Result: Auth2Result, sa: SavedAccount, ): Boolean { + log.i("afterAccessTokenUpdate token ${sa.bearerAccessToken ?: sa.misskeyApiToken} =>${auth2Result.tokenJson}") // DBの情報を更新する authRepo.updateTokenInfo(sa, auth2Result) @@ -275,7 +279,7 @@ private fun ActMain.afterAccessTokenUpdate( return true } -private fun ActMain.afterAccountAdd( +private suspend fun ActMain.afterAccountAdd( newAcct: Acct, auth2Result: Auth2Result, ): Boolean { @@ -350,17 +354,22 @@ fun ActMain.handleSharedIntent(intent: Intent) { } // アカウントを追加/更新したらappServerHashの取得をやりなおす -fun ActMain.updatePushDistributer() { +suspend fun ActMain.updatePushDistributer() { when { fcmHandler.noFcm && prefDevice.pushDistributor.isNullOrEmpty() -> { - try { - selectPushDistributor() - // 選択したら - } catch (_: CancellationException) { - // 選択しなかった場合は購読の更新を行わない + selectPushDistributor() + // 選択しなかった場合は購読の更新を行わない + } + else -> { + runInProgress(cancellable = false) { reporter -> + withContext(AppDispatchers.DEFAULT) { + pushRepo.switchDistributor( + prefDevice.pushDistributor, + reporter = reporter + ) + } } } - else -> PushWorker.enqueueRegisterEndpoint(this) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt index d99f6d14..6cfd8cf5 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/auth/AuthMastodon.kt @@ -23,6 +23,9 @@ class AuthMastodon(override val client: TootApiClient) : AuthBase() { companion object { private val log = LogCategory("MastodonAuth") + @Suppress("MayBeConstant") + val DEBUG_AUTH = false + const val callbackUrl = "${BuildConfig.customScheme}://oauth/" fun mastodonScope(ti: TootInstance?) = when { @@ -153,6 +156,7 @@ class AuthMastodon(override val client: TootApiClient) : AuthBase() { put(KEY_AUTH_VERSION, AUTH_VERSION) put(KEY_CLIENT_SCOPE, scopeString) } + if(DEBUG_AUTH) log.i("DEBUG_AUTH client_id=${clientInfo.string("client_id")}") // client credentialを取得して保存する // この時点ではまだ client credential がないので、必ず更新と保存が行われる prepareClientCredential(apiHost, clientInfo, clientName) @@ -291,6 +295,7 @@ class AuthMastodon(override val client: TootApiClient) : AuthBase() { val accessToken = tokenInfo.string("access_token") ?.notEmpty() ?: error("can't parse access token.") + if(DEBUG_AUTH) log.i("DEBUG_AUTH accessToken=${accessToken}") val accountJson = verifyAccount( accessToken = accessToken, diff --git a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppDataExporter.kt b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppDataExporter.kt index a26c6271..4bdec4a4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppDataExporter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/appsetting/AppDataExporter.kt @@ -115,7 +115,7 @@ object AppDataExporter { writer.name(jsonKey) writer.beginArray() - appDatabase.rawQuery("select from $table", emptyArray()).use { cursor -> + appDatabase.rawQuery("select * from $table", emptyArray()).use { cursor -> val names = ArrayList() val column_count = cursor.columnCount for (i in 0 until column_count) { 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 7bd635a2..b871d29a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/Column.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/Column.kt @@ -68,7 +68,7 @@ class Column( val account_db_id = src.long(ColumnEncoder.KEY_ACCOUNT_ROW_ID) ?: -1L return if (account_db_id > 0) { daoSavedAccount.loadAccount(account_db_id) - ?: error("missing account") + ?: error("missing account for db_id $account_db_id") } else { SavedAccount.na } diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderBase.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderBase.kt index dd9752e3..cf4cd278 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderBase.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderBase.kt @@ -9,6 +9,7 @@ import jp.juggler.subwaytooter.api.entity.TootAccountRef import jp.juggler.subwaytooter.column.Column import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.util.log.LogCategory +import jp.juggler.util.log.withCaption import jp.juggler.util.ui.scan abstract class ViewHolderHeaderBase(viewRoot: View) : RecyclerView.ViewHolder(viewRoot) { @@ -28,15 +29,18 @@ abstract class ViewHolderHeaderBase(viewRoot: View) : RecyclerView.ViewHolder(vi if (v is Button) { // ボタンは太字なので触らない } else if (v is TextView) { - v.typeface = ActMain.timelineFont + try { + activity.timelineFontSizeSp + .takeIf { it.isFinite() } + ?.let { v.textSize = it } - activity.timelineFontSizeSp - .takeIf { it.isFinite() } - ?.let { v.textSize = it } - - activity.timelineSpacing - ?.let { v.setLineSpacing(0f, it) } + activity.timelineSpacing + ?.let { v.setLineSpacing(0f, it) } + } catch (ex: NullPointerException) { + // 非null型なのになぜかnull例外が出る + log.w(ex.withCaption("can't read timelineFontSizeSp, timelineSpacing")) + } } } catch (ex: Throwable) { log.e(ex, "can't initialize text styles.") diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderInstance.kt index e866b379..0d2bfbcd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ViewHolderHeaderInstance.kt @@ -91,6 +91,7 @@ internal class ViewHolderHeaderInstance( btnExplore.isEnabledAlpha = false tvConfiguration.text = "" tvFedibirdCapacities.text = "" + tvPlelomaFeatures.text = "" } else { val domain = instance.apDomain btnInstance.text = when { @@ -163,6 +164,8 @@ internal class ViewHolderHeaderInstance( instance.configuration?.toString(1, sort = true) ?: "" tvFedibirdCapacities.text = instance.fedibirdCapabilities?.sorted()?.joinToString("\n") ?: "" + tvPlelomaFeatures.text= + instance.pleromaFeatures?.sorted()?.joinToString("\n") ?: "" } tvHandshake.text = when (val handshake = column.handshake) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/push/PushBase.kt b/app/src/main/java/jp/juggler/subwaytooter/push/PushBase.kt index 37f00eb1..9dde80e2 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushBase.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushBase.kt @@ -33,12 +33,13 @@ abstract class PushBase { protected abstract val daoStatus: AccountNotificationStatus.Access // 購読の確認と更新 + // 失敗したらエラーメッセージを返す。成功したらnull abstract suspend fun updateSubscription( subLog: SubscriptionLogger, account: SavedAccount, willRemoveSubscription: Boolean, forceUpdate:Boolean, - ) + ):String? // プッシュメッセージのJSONデータを通知用に整形 abstract suspend fun formatPushMessage( diff --git a/app/src/main/java/jp/juggler/subwaytooter/push/PushMastodon.kt b/app/src/main/java/jp/juggler/subwaytooter/push/PushMastodon.kt index 910cb4e2..b03c5500 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushMastodon.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushMastodon.kt @@ -8,12 +8,16 @@ import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.api.ApiError import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiClient +import jp.juggler.subwaytooter.api.auth.AuthMastodon import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.push.ApiPushMastodon import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.lazyContext import jp.juggler.subwaytooter.pref.prefDevice -import jp.juggler.subwaytooter.table.* +import jp.juggler.subwaytooter.table.AccountNotificationStatus +import jp.juggler.subwaytooter.table.PushMessage +import jp.juggler.subwaytooter.table.SavedAccount +import jp.juggler.subwaytooter.table.appDatabase import jp.juggler.util.data.* import jp.juggler.util.log.LogCategory import jp.juggler.util.time.parseTimeIso8601 @@ -41,26 +45,25 @@ class PushMastodon( account: SavedAccount, willRemoveSubscription: Boolean, forceUpdate: Boolean, - ) { + ): String? { val deviceHash = deviceHash(account) val newUrl = snsCallbackUrl(account) // appServerHashを参照する if (newUrl.isNullOrEmpty()) { - if (willRemoveSubscription) { - val msg = - lazyContext.getString(R.string.push_subscription_app_server_hash_missing_but_ok) - subLog.i(msg) - } else { - val msg = - lazyContext.getString(R.string.push_subscription_app_server_hash_missing_error) - subLog.e(msg) - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - msg + return when { + willRemoveSubscription -> { + val msg = lazyContext.getString( + R.string.push_subscription_app_server_hash_missing_but_ok + ) + subLog.i(msg) + null + } + else -> lazyContext.getString( + R.string.push_subscription_app_server_hash_missing_error ) } - return } + if (AuthMastodon.DEBUG_AUTH) log.i("DEBUG_AUTH bearerAccessToken=${account.bearerAccessToken} ${account.acct}") val oldSubscription = try { api.getPushSubscription(account) } catch (ex: Throwable) { @@ -90,16 +93,12 @@ class PushMastodon( } } if (params["dh"] != deviceHash && !isOldSubscription(account, oldEndpointUrl)) { - // この端末で作成した購読ではない。 // TODO: 古い形式のURLを移行できないか? log.w("deviceHash not match. keep it for other devices. ${account.acct} $oldEndpointUrl") - subLog.e(R.string.push_subscription_exists_but_not_created_by_this_device) - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - context.getString(R.string.push_subscription_exists_but_not_created_by_this_device) + return context.getString( + R.string.push_subscription_exists_but_not_created_by_this_device ) - return } } } @@ -114,14 +113,22 @@ class PushMastodon( api.deletePushSubscription(account) } } - return + return null } - val newAlerts = account.alerts() + // サーバのバージョンを見て、サーバの知らないalertを無視して比較する + val client = TootApiClient(context, callback = object : TootApiCallback { + override suspend fun isApiCancelled(): Boolean = !coroutineContext.isActive + }) + client.account = account + val ti = TootInstance.getExOrThrow(client) + + val newAlerts = account.alerts(ti) val isSameAlert = isSameAlerts( subLog = subLog, account = account, + ti = ti, oldSubscriptionJson = oldSubscription, newAlerts = newAlerts, ) @@ -135,7 +142,7 @@ class PushMastodon( ) { // 現在の更新を使い続ける subLog.i(R.string.push_subscription_keep_using) - return + return null } if (newUrl == oldEndpointUrl) { @@ -179,6 +186,7 @@ class PushMastodon( ) subLog.i(R.string.push_subscription_completed) } + return null } private fun isOldSubscription(account: SavedAccount, url: String): Boolean { @@ -189,7 +197,7 @@ class PushMastodon( // /{flags } // /{ client identifier} - val clientIdentifierOld = url.toHttpUrlOrNull()?.pathSegments?.elementAtOrNull(4) + val clientIdentifierOld = url.toHttpUrlOrNull()?.pathSegments?.elementAtOrNull(4) ?: return false val installId = prefDevice.installIdV1?.notEmpty() ?: return false val accessToken = account.bearerAccessToken?.notEmpty() ?: return false @@ -197,9 +205,10 @@ class PushMastodon( return clientIdentifier == clientIdentifierOld } - private suspend fun isSameAlerts( + private fun isSameAlerts( subLog: SubscriptionLogger, account: SavedAccount, + ti: TootInstance, oldSubscriptionJson: JsonObject?, newAlerts: JsonObject, ): Boolean { @@ -210,7 +219,7 @@ class PushMastodon( // flagsの値が変わりendpoint URLも変わった状態で購読を自動更新してしまう // しかしそのタイミングではサーバは古いのでサーバ側の購読内容は変化しなかった。 - // サーバ上の購読アラートのリスト + // 既存の購読のアラートのリスト var alertsOld = oldSubscription.alerts.entries .mapNotNull { if (it.value) it.key else null } .sorted() @@ -221,26 +230,13 @@ class PushMastodon( .sorted() // 両方に共通するアラートは除去する + // サーバが知らないアラートは除去する val bothHave = alertsOld.filter { alertsNew.contains(it) } - alertsOld = alertsOld.filter { !bothHave.contains(it) } - alertsNew = alertsNew.filter { !bothHave.contains(it) } - - // サーバのバージョンを調べる前に、この時点でalertsが一致するか確認する - if (alertsOld.joinToString(",") == alertsNew.joinToString(",")) { - return true - } - - // サーバのバージョンを見て、サーバの知らないalertを無視して比較する - val client = TootApiClient(context, callback = object : TootApiCallback { - override suspend fun isApiCancelled(): Boolean = !coroutineContext.isActive - }) - client.account = account - val ti = TootInstance.getExOrThrow(client) - - alertsOld = alertsOld.knownOnly(account, ti) - alertsNew = alertsNew.knownOnly(account, ti) + alertsOld = + alertsOld.filter { !bothHave.contains(it) }.knownOnly(account, ti) + alertsNew = + alertsNew.filter { !bothHave.contains(it) }.knownOnly(account, ti) return if (alertsOld.joinToString(",") == alertsNew.joinToString(",")) { - log.i("${account.acct}: same alerts(2)") true } else { log.i("${account.acct}: changed. old=${alertsOld.sorted()}, new=${alertsNew.sorted()}") @@ -249,32 +245,41 @@ class PushMastodon( } } - private fun SavedAccount.alerts() = JsonObject().apply { + private fun SavedAccount.alerts(ti: TootInstance) = JsonObject().also { dst -> // Mastodon's Notification::TYPES in // in https://github.com/mastodon/mastodon/blob/main/app/models/notification.rb#L30 - put(TootNotification.TYPE_ADMIN_REPORT, notificationFollow) - put(TootNotification.TYPE_ADMIN_SIGNUP, notificationFollow) // 設定項目不足 - put(TootNotification.TYPE_FAVOURITE, notificationFavourite) - put(TootNotification.TYPE_FOLLOW, notificationFollow) - put(TootNotification.TYPE_FOLLOW_REQUEST, notificationFollowRequest) - put(TootNotification.TYPE_MENTION, notificationMention) - put(TootNotification.TYPE_POLL, notificationVote) - put(TootNotification.TYPE_REBLOG, notificationBoost) - put(TootNotification.TYPE_STATUS, notificationPost) - put(TootNotification.TYPE_UPDATE, notificationUpdate) + dst[TootNotification.TYPE_ADMIN_REPORT] = notificationFollow + dst[TootNotification.TYPE_ADMIN_SIGNUP] = notificationFollow // 設定項目不足 + dst[TootNotification.TYPE_FAVOURITE] = notificationFavourite + dst[TootNotification.TYPE_FOLLOW] = notificationFollow + dst[TootNotification.TYPE_FOLLOW_REQUEST] = notificationFollowRequest + dst[TootNotification.TYPE_MENTION] = notificationMention + dst[TootNotification.TYPE_POLL] = notificationVote + dst[TootNotification.TYPE_REBLOG] = notificationBoost + dst[TootNotification.TYPE_STATUS] = notificationPost + dst[TootNotification.TYPE_UPDATE] = notificationUpdate // fedibird拡張 // https://github.com/fedibird/mastodon/blob/fedibird/app/controllers/api/v1/push/subscriptions_controller.rb#L55 // https://github.com/fedibird/mastodon/blob/fedibird/app/models/notification.rb - put(TootNotification.TYPE_EMOJI_REACTION, notificationReaction) - put(TootNotification.TYPE_SCHEDULED_STATUS, notificationPost) // 設定項目不足 - put(TootNotification.TYPE_STATUS_REFERENCE, notificationStatusReference) + if (!ti.pleromaFeatures.isNullOrEmpty()) { + dst[TootNotification.TYPE_EMOJI_REACTION_PLEROMA] = notificationReaction + } else if (!ti.fedibirdCapabilities.isNullOrEmpty()) { + dst[TootNotification.TYPE_EMOJI_REACTION] = notificationReaction + } + dst[TootNotification.TYPE_SCHEDULED_STATUS] = notificationPost // 設定項目不足 + dst[TootNotification.TYPE_STATUS_REFERENCE] = notificationStatusReference } // サーバが知らないアラート種別は比較対象から除去する - private fun Iterable.knownOnly(account: SavedAccount, ti: TootInstance) = filter { + private fun Iterable.knownOnly( + account: SavedAccount, + ti: TootInstance, + ) = filter { when (it) { + // TYPE_ADMIN_SIGNUP, TYPE_UPDATE はalertから読めない時期があった。4.0.0以降なら大丈夫だろう + TootNotification.TYPE_ADMIN_REPORT -> ti.versionGE(TootInstance.VERSION_4_0_0) - TootNotification.TYPE_ADMIN_SIGNUP -> ti.versionGE(TootInstance.VERSION_3_5_0_rc1) + TootNotification.TYPE_ADMIN_SIGNUP -> ti.versionGE(TootInstance.VERSION_4_0_0) TootNotification.TYPE_FAVOURITE -> true TootNotification.TYPE_FOLLOW -> true TootNotification.TYPE_FOLLOW_REQUEST -> ti.versionGE(TootInstance.VERSION_3_1_0_rc1) @@ -282,12 +287,15 @@ class PushMastodon( TootNotification.TYPE_POLL -> ti.versionGE(TootInstance.VERSION_2_8_0_rc1) TootNotification.TYPE_REBLOG -> true TootNotification.TYPE_STATUS -> ti.versionGE(TootInstance.VERSION_3_3_0_rc1) - TootNotification.TYPE_UPDATE -> ti.versionGE(TootInstance.VERSION_3_5_0_rc1) + TootNotification.TYPE_UPDATE -> ti.versionGE(TootInstance.VERSION_4_0_0) ////////////////////// // Fedibird拡張 TootNotification.TYPE_EMOJI_REACTION, + -> InstanceCapability.emojiReaction(account, ti) + + // pleromaの絵文字リアクションはalertに指定できない TootNotification.TYPE_EMOJI_REACTION_PLEROMA, -> InstanceCapability.emojiReaction(account, ti) 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 8f4db2d0..2cbddbc4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushMisskey.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushMisskey.kt @@ -42,7 +42,7 @@ class PushMisskey( account: SavedAccount, willRemoveSubscription: Boolean, forceUpdate: Boolean, - ) { + ): String? { val newUrl = snsCallbackUrl(account) val lastEndpointUrl = daoStatus.lastEndpointUrl(account.acct) @@ -83,29 +83,28 @@ class PushMisskey( subLog.i(R.string.push_subscription_off_unread_notification) } } - return + return null } else { // 古い購読はあったが、削除したい api.deletePushSubscription(account, lastEndpointUrl) daoStatus.deleteLastEndpointUrl(account.acct) if (willRemoveSubscription) { subLog.i(R.string.push_subscription_delete_current) - return + return null } } } } if (newUrl == null) { - if (willRemoveSubscription) { - subLog.i(R.string.push_subscription_app_server_hash_missing_but_ok) - } else { - subLog.e(R.string.push_subscription_app_server_hash_missing_error) - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - context.getString(R.string.push_subscription_app_server_hash_missing_error) + return when { + willRemoveSubscription -> { + subLog.i(R.string.push_subscription_app_server_hash_missing_but_ok) + null + } + else -> context.getString( + R.string.push_subscription_app_server_hash_missing_error ) } - return } else if (willRemoveSubscription) { // 購読を解除したい。 // hasEmptySubscription が真なら購読はないが、 @@ -117,7 +116,7 @@ class PushMisskey( subLog.i(R.string.push_subscription_is_not_required_delete_secret_keys) } } - return + return null } // 鍵がなければ作る @@ -163,6 +162,7 @@ class PushMisskey( subLog.i(R.string.push_subscription_server_key_saved) } subLog.i(R.string.push_subscription_completed) + return null } private fun isOldSubscription(account: SavedAccount, url: String): Boolean { @@ -173,7 +173,7 @@ class PushMisskey( // /{flags } // /{ client identifier} - val clientIdentifierOld = url.toHttpUrlOrNull()?.pathSegments?.elementAtOrNull(4) + val clientIdentifierOld = url.toHttpUrlOrNull()?.pathSegments?.elementAtOrNull(4) ?: return false val installId = prefDevice.installIdV1?.notEmpty() ?: return false val accessToken = account.misskeyApiToken?.notEmpty() ?: return false 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 b143470c..7f05b92f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/push/PushRepo.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/push/PushRepo.kt @@ -36,12 +36,15 @@ import jp.juggler.util.os.applicationContextSafe import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.unifiedpush.android.connector.UnifiedPush import java.lang.ref.WeakReference import java.security.Provider import java.util.concurrent.TimeUnit +import kotlin.math.min private val log = LogCategory("PushRepo") @@ -90,6 +93,8 @@ class PushRepo( private val ncPushMessage = NotificationChannels.PushMessage var refReporter: WeakReference? = null + + var subscriptionMutex = Mutex() } private val pushMisskey by lazy { @@ -116,7 +121,7 @@ class PushRepo( * UPでプッシュサービスを選ぶと呼ばれる */ suspend fun switchDistributor( - pushDistributor: String, + pushDistributor: String?, reporter: SuspendProgress.Reporter, ) { val timeSwitchStart = System.currentTimeMillis() @@ -139,6 +144,10 @@ class PushRepo( fcmHandler.deleteFcmToken() when (pushDistributor) { + null, "" -> { + // 特に変更しない(アクセストークン更新時に呼ばれる + enqueueRegisterEndpoint(context) + } PrefDevice.PUSH_DISTRIBUTOR_NONE -> { // 購読解除 reporter.setMessage("SubscriptionUpdateService.launch") @@ -161,20 +170,19 @@ class PushRepo( } val timeout = timeSwitchStart + TimeUnit.SECONDS.toMillis(20) while (true) { - val now = System.currentTimeMillis() - if (now >= timeout) { - reporter.setMessage("timeout") - delay(888L) - break - } if (PushWorker.timeEndRegisterEndpoint.get() >= timeSwitchStart || PushWorker.timeEndUpEndpoint.get() >= timeSwitchStart ) { - reporter.setMessage("complete") - delay(888L) + // complete break } - delay(1000L) + val remain = min(1000L, timeout - System.currentTimeMillis()) + if (remain <= 0) { + reporter.setMessage("timeout") + delay(666L) + break + } + delay(remain) } } @@ -215,155 +223,158 @@ class PushRepo( suspend fun registerEndpoint( keepAliveMode: Boolean, ) { - log.i("registerEndpoint: keepAliveMode=$keepAliveMode") + subscriptionMutex.withLock { + log.i("registerEndpoint: keepAliveMode=$keepAliveMode") - // 古いFCMトークンの情報はアプリサーバ側で勝手に消えるはず - try { - // 期限切れのUPエンドポイントがあればそれ経由の中継を解除する - prefDevice.fcmTokenExpired.notEmpty()?.let { - refReporter?.get()?.setMessage("期限切れのFCMデバイストークンをアプリサーバから削除しています") - log.i("remove fcmTokenExpired") - apiAppServer.endpointRemove(fcmToken = it) - prefDevice.fcmTokenExpired = null + // 古いFCMトークンの情報はアプリサーバ側で勝手に消えるはず + try { + // 期限切れのUPエンドポイントがあればそれ経由の中継を解除する + prefDevice.fcmTokenExpired.notEmpty()?.let { + refReporter?.get()?.setMessage("期限切れのFCMデバイストークンをアプリサーバから削除しています") + log.i("remove fcmTokenExpired") + apiAppServer.endpointRemove(fcmToken = it) + prefDevice.fcmTokenExpired = null + } + } catch (ex: Throwable) { + log.w(ex, "can't forgot fcmTokenExpired") } - } catch (ex: Throwable) { - log.w(ex, "can't forgot fcmTokenExpired") - } - try { - // 期限切れのUPエンドポイントがあればそれ経由の中継を解除する - prefDevice.upEndpointExpired.notEmpty()?.let { - refReporter?.get()?.setMessage("期限切れのUnifiedPushエンドポイントをアプリサーバから削除しています") - log.i("remove upEndpointExpired") - apiAppServer.endpointRemove(upUrl = it) - prefDevice.upEndpointExpired = null + try { + // 期限切れのUPエンドポイントがあればそれ経由の中継を解除する + prefDevice.upEndpointExpired.notEmpty()?.let { + refReporter?.get()?.setMessage("期限切れのUnifiedPushエンドポイントをアプリサーバから削除しています") + log.i("remove upEndpointExpired") + apiAppServer.endpointRemove(upUrl = it) + prefDevice.upEndpointExpired = null + } + } catch (ex: Throwable) { + log.w(ex, "can't forgot upEndpointExpired") } - } catch (ex: Throwable) { - log.w(ex, "can't forgot upEndpointExpired") - } - val realAccounts = daoSavedAccount.loadRealAccounts() - .filter { !it.isPseudo && it.isConfirmed } + val realAccounts = daoSavedAccount.loadRealAccounts() + .filter { !it.isPseudo && it.isConfirmed } - val accts = realAccounts.map { it.acct } + val accts = realAccounts.map { it.acct } - // map of acctHash to account - val acctHashMap = daoStatus.updateAcctHash(accts) - if (acctHashMap.isEmpty()) { - log.w("acctHashMap is empty. no need to update register endpoint") - return - } - - if (keepAliveMode) { - val lastUpdated = prefDevice.timeLastEndpointRegister - val now = System.currentTimeMillis() - if (now - lastUpdated < TimeUnit.DAYS.toMillis(3)) { - log.i("lazeMode: skip re-registration.") + // map of acctHash to account + val acctHashMap = daoStatus.updateAcctHash(accts) + if (acctHashMap.isEmpty()) { + log.w("acctHashMap is empty. no need to update register endpoint") + return } - } - var willRemoveSubscription = false + if (keepAliveMode) { + val lastUpdated = prefDevice.timeLastEndpointRegister + val now = System.currentTimeMillis() + if (now - lastUpdated < TimeUnit.DAYS.toMillis(3)) { + log.i("lazeMode: skip re-registration.") + } + } - // アプリサーバにendpointを登録する - refReporter?.get()?.setMessage("アプリサーバにプッシュサービスの情報を送信しています") + var willRemoveSubscription = false - if (!fcmHandler.hasFcm && prefDevice.pushDistributor == PrefDevice.PUSH_DISTRIBUTOR_FCM) { - log.w("fmc selected, but this is noFcm build. unset distributer.") - prefDevice.pushDistributor = null - } + // アプリサーバにendpointを登録する + refReporter?.get()?.setMessage("アプリサーバにプッシュサービスの情報を送信しています") - val acctHashList = acctHashMap.keys.toList() + if (!fcmHandler.hasFcm && prefDevice.pushDistributor == PrefDevice.PUSH_DISTRIBUTOR_FCM) { + log.w("fmc selected, but this is noFcm build. unset distributer.") + prefDevice.pushDistributor = null + } - val json = when (prefDevice.pushDistributor) { - null, "" -> when { - fcmHandler.hasFcm -> { - log.i("registerEndpoint dist=FCM(default), acctHashList=${acctHashList.size}") + val acctHashList = acctHashMap.keys.toList() + + val json = when (prefDevice.pushDistributor) { + null, "" -> when { + 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.") + return + } + } + PrefDevice.PUSH_DISTRIBUTOR_NONE -> { + log.i("push distrobuter 'none' is selected. it will remove subscription.") + willRemoveSubscription = true + null + } + PrefDevice.PUSH_DISTRIBUTOR_FCM -> { + log.i("registerEndpoint dist=FCM, acctHashList=${acctHashList.size}") registerEndpointFcm(acctHashList) } else -> { - log.w("pushDistributor not selected. but can't select default distributor from background service.") - return + log.i("registerEndpoint dist=${prefDevice.pushDistributor}, acctHashList=${acctHashList.size}") + registerEndpointUnifiedPush(acctHashList) } } - PrefDevice.PUSH_DISTRIBUTOR_NONE -> { - log.i("push distrobuter 'none' is selected. it will remove subscription.") - willRemoveSubscription = true - null - } - 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.") + when { + json.isNullOrEmpty() -> + log.i("no information of appServerHash.") - else -> { - // acctHash => appServerHash のマップが返ってくる - // ステータスに覚える - var saveCount = 0 - for (acctHash in json.keys) { - val acct = acctHashMap[acctHash] ?: continue - val appServerHash = json.string(acctHash) ?: continue - ++saveCount - val status = daoStatus.loadOrCreate(acct) - if (status.appServerHash == appServerHash) continue - daoStatus.saveAppServerHash(status.id, appServerHash) + else -> { + // acctHash => appServerHash のマップが返ってくる + // ステータスに覚える + var saveCount = 0 + for (acctHash in json.keys) { + val acct = acctHashMap[acctHash] ?: continue + val appServerHash = json.string(acctHash) ?: continue + ++saveCount + val status = daoStatus.loadOrCreate(acct) + if (status.appServerHash == appServerHash) continue + daoStatus.saveAppServerHash(status.id, appServerHash) + } + log.i("appServerHash updated. saveCount=$saveCount") } - log.i("appServerHash updated. saveCount=$saveCount") } - } - realAccounts.forEach { account -> - val subLog = object : PushBase.SubscriptionLogger { - override val context = this@PushRepo.context - override fun i(msg: String) { - log.i("[${account.acct}]$msg") + realAccounts.forEach { account -> + val subLog = object : PushBase.SubscriptionLogger { + override val context = this@PushRepo.context + override fun i(msg: String) { + log.i("[${account.acct}]$msg") + refReporter?.get()?.setMessage("[${account.acct}]$msg") + } + + override fun e(msg: String) { + log.e("[${account.acct}]$msg") + refReporter?.get()?.setMessage("[${account.acct}]$msg") + daoAccountNotificationStatus.updateSubscriptionError( + account.acct, + msg + ) + } + + override fun e(ex: Throwable, msg: String) { + log.e(ex, "[${account.acct}]$msg") + refReporter?.get()?.setMessage("[${account.acct}]$msg") + daoAccountNotificationStatus.updateSubscriptionError( + account.acct, + ex.withCaption(msg) + ) + } } - - override fun e(msg: String) { - log.e("[${account.acct}]$msg") - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - msg - ) - } - - override fun e(ex: Throwable, msg: String) { - log.e(ex, "[${account.acct}]$msg") - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - ex.withCaption(msg) + try { + val errMsg = pushBase(account).updateSubscription( + subLog = subLog, + account = account, + willRemoveSubscription = willRemoveSubscription || !account.isRequiredPushSubscription(), + forceUpdate = false, ) + daoAccountNotificationStatus.updateSubscriptionError(account.acct, errMsg) + if (errMsg != null) { + subLog.e(errMsg) + } + } catch (ex: Throwable) { + log.e(ex, "updateSubscription failed.") + val errMsg = ex.withCaption() + subLog.e(errMsg) + daoAccountNotificationStatus.updateSubscriptionError(account.acct, errMsg) } } - try { - refReporter?.get()?.setMessage("${account.acct.pretty} のWebPush購読を更新しています") - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - null - ) - pushBase(account).updateSubscription( - subLog = subLog, - account = account, - willRemoveSubscription = willRemoveSubscription || !account.isRequiredPushSubscription(), - forceUpdate = false, - ) - } catch (ex: Throwable) { - subLog.e(ex, "updateSubscription failed.") - daoAccountNotificationStatus.updateSubscriptionError( - account.acct, - ex.withCaption() - ) - } + prefDevice.timeLastEndpointRegister = System.currentTimeMillis() } - prefDevice.timeLastEndpointRegister = System.currentTimeMillis() } private suspend fun registerEndpointUnifiedPush(acctHashList: List) = @@ -408,12 +419,25 @@ class PushRepo( willRemoveSubscription: Boolean, forceUpdate: Boolean = false, ) { - pushBase(account).updateSubscription( - subLog = subLog, - account = account, - willRemoveSubscription = willRemoveSubscription || !account.isRequiredPushSubscription(), - forceUpdate = forceUpdate, - ) + subscriptionMutex.withLock { + try { + val errMsg = pushBase(account).updateSubscription( + subLog = subLog, + account = account, + willRemoveSubscription = willRemoveSubscription || !account.isRequiredPushSubscription(), + forceUpdate = forceUpdate, + ) + daoAccountNotificationStatus.updateSubscriptionError(account.acct, errMsg) + if (errMsg != null) { + subLog.e(errMsg) + } + } catch (ex: Throwable) { + log.e(ex, "updateSubscription failed.") + val errMsg = ex.withCaption() + subLog.e(errMsg) + daoAccountNotificationStatus.updateSubscriptionError(account.acct, errMsg) + } + } } private fun pushBase(account: SavedAccount) = when { 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 cc35bd1f..b0861110 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.kt @@ -38,6 +38,7 @@ class SavedAccount( apiHostArg: String? = null, apDomainArg: String? = null, + var accountJson:JsonObject? = null, var confirmBoost: Boolean = false, var confirmFavourite: Boolean = false, var confirmFollow: Boolean = false, @@ -119,7 +120,7 @@ class SavedAccount( var notificationAccentColor by jsonDelegates.int init { - log.i("ctor acctArg $acctArg") + // log.i("ctor acctArg $acctArg") // acctArg はMastodonの生のやつで、ドメイン部分がない場合がある // Acct.parse はHost部分がnullのacctになるかもしれない @@ -142,7 +143,8 @@ class SavedAccount( acctArg = cursor.getString(COL_USER), // acct apiHostArg = cursor.getStringOrNull(COL_HOST), // host apDomainArg = cursor.getStringOrNull(COL_DOMAIN), // host - misskeyVersion = cursor.getInt(COL_MISSKEY_VERSION), + + accountJson = cursor.getStringOrNull(COL_ACCOUNT)?.decodeJsonObject(), confirmBoost = cursor.getBoolean(COL_CONFIRM_BOOST), confirmFavourite = cursor.getBoolean(COL_CONFIRM_FAVOURITE), confirmFollow = cursor.getBoolean(COL_CONFIRM_FOLLOW), @@ -177,6 +179,9 @@ class SavedAccount( tokenJson = cursor.getString(COL_TOKEN).decodeJsonObject(), visibility = TootVisibility.parseSavedVisibility(cursor.getStringOrNull(COL_VISIBILITY)) ?: TootVisibility.Public, + + misskeyVersion = cursor.getInt(COL_MISSKEY_VERSION), + // lastNotificationError = cursor.getStringOrNull(COL_LAST_NOTIFICATION_ERROR) // last_push_endpoint = cursor.getStringOrNull(COL_LAST_PUSH_ENDPOINT) // last_subscription_error = cursor.getStringOrNull(COL_LAST_SUBSCRIPTION_ERROR) @@ -185,9 +190,7 @@ class SavedAccount( // register_time = cursor.getLong(COL_REGISTER_TIME) ) { - val strAccount = cursor.getString(COL_ACCOUNT) - val jsonAccount = strAccount.decodeJsonObject() - loginAccount = if (jsonAccount["id"] == null) { + loginAccount = if (accountJson?.get("id") == null) { null // 疑似アカウント } else { TootParser( @@ -197,8 +200,8 @@ class SavedAccount( apDomainArg = this@SavedAccount.apDomain, misskeyVersion = misskeyVersion ) - ).account(jsonAccount) - ?: error("missing loginAccount for $strAccount") + ).account(accountJson) + ?: error("missing loginAccount for $accountJson") } } @@ -468,6 +471,7 @@ class SavedAccount( if (isInvalidId) error("saveSetting: missing db_id") ContentValues().apply { + put(COL_ACCOUNT,accountJson?.toString()) put(COL_CONFIRM_BOOST, confirmBoost) put(COL_CONFIRM_FAVOURITE, confirmFavourite) put(COL_CONFIRM_FOLLOW, confirmFollow) @@ -499,7 +503,9 @@ class SavedAccount( put(COL_NOTIFICATION_VOTE, notificationVote) put(COL_PUSH_POLICY, pushPolicy) // put(COL_SOUND_URI, soundUri) + put(COL_TOKEN,tokenJson?.toString()) put(COL_VISIBILITY, visibility.id.toString()) + put(COL_MISSKEY_VERSION,misskeyVersion) }.let { db.update(table, it, "$COL_ID=?", arrayOf(db_id.toString())) } } } @@ -512,7 +518,7 @@ class SavedAccount( // DBから削除されてるかもしれない val b = newData ?: loadAccount(db_id) ?: return - + this.accountJson = b.accountJson this.confirmBoost = b.confirmBoost this.confirmFavourite = b.confirmFavourite this.confirmPost = b.confirmPost @@ -525,31 +531,32 @@ class SavedAccount( this.dontHideNsfw = b.dontHideNsfw this.dontShowTimeout = b.dontShowTimeout this.expandCw = b.expandCw + this.extraJson = b.extraJson this.imageMaxMegabytes = b.imageMaxMegabytes this.imageResize = b.imageResize this.lang = b.lang + this.movieMaxMegabytes = b.movieMaxMegabytes this.movieTranscodeBitrate = b.movieTranscodeBitrate this.movieTranscodeFramerate = b.movieTranscodeFramerate this.movieTranscodeMode = b.movieTranscodeMode this.movieTranscodeSquarePixels = b.movieTranscodeSquarePixels - this.movieMaxMegabytes = b.movieMaxMegabytes this.notificationBoost = b.notificationBoost this.notificationFavourite = b.notificationFavourite this.notificationFollow = b.notificationFollow this.notificationFollowRequest = b.notificationFollowRequest this.notificationMention = b.notificationMention this.notificationPost = b.notificationPost + this.notificationPullEnable = b.notificationPullEnable + this.notificationPushEnable = b.notificationPushEnable this.notificationReaction = b.notificationReaction this.notificationStatusReference = b.notificationStatusReference this.notificationUpdate = b.notificationUpdate this.notificationVote = b.notificationVote this.pushPolicy = b.pushPolicy + // this.soundUri = b.soundUri this.tokenJson = b.tokenJson this.visibility = b.visibility - this.notificationPushEnable = b.notificationPushEnable - this.notificationPullEnable = b.notificationPullEnable - - // this.soundUri = b.soundUri + this.misskeyVersion = b.misskeyVersion } } diff --git a/app/src/main/res/layout/lv_header_instance.xml b/app/src/main/res/layout/lv_header_instance.xml index 7803c79e..f427ebf7 100644 --- a/app/src/main/res/layout/lv_header_instance.xml +++ b/app/src/main/res/layout/lv_header_instance.xml @@ -5,87 +5,76 @@ android:layout_height="wrap_content" android:descendantFocusability="blocksDescendants" android:orientation="vertical" + android:paddingBottom="128dp" + android:paddingEnd="12dp" android:paddingStart="12dp" android:paddingTop="12dp" - android:paddingEnd="12dp" - android:paddingBottom="128dp" > - + + android:text="@string/instance" />