通知のアクセント色をリソースXMLに移す

This commit is contained in:
tateisu 2023-02-06 19:26:02 +09:00
parent d520f9ddfd
commit 60bacaac64
6 changed files with 525 additions and 526 deletions

View File

@ -21,8 +21,8 @@ import jp.juggler.subwaytooter.databinding.ActPushMessageListBinding
import jp.juggler.subwaytooter.databinding.LvPushMessageBinding import jp.juggler.subwaytooter.databinding.LvPushMessageBinding
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.runInProgress import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.notification.NotificationIconAndColor import jp.juggler.subwaytooter.notification.PushMessageIconColor
import jp.juggler.subwaytooter.notification.notificationIconAndColor import jp.juggler.subwaytooter.notification.iconColor
import jp.juggler.subwaytooter.push.pushRepo import jp.juggler.subwaytooter.push.pushRepo
import jp.juggler.subwaytooter.table.PushMessage import jp.juggler.subwaytooter.table.PushMessage
import jp.juggler.subwaytooter.table.daoAccountNotificationStatus import jp.juggler.subwaytooter.table.daoAccountNotificationStatus
@ -165,11 +165,13 @@ class ActPushMessageList : AppCompatActivity() {
} }
private val tintIconMap = HashMap<String, Drawable>() private val tintIconMap = HashMap<String, Drawable>()
fun tintIcon(ic: NotificationIconAndColor) =
fun tintIcon(ic: PushMessageIconColor) =
tintIconMap.getOrPut(ic.name) { tintIconMap.getOrPut(ic.name) {
val src = ContextCompat.getDrawable(this@ActPushMessageList, ic.iconId)!! val context = this
val src = ContextCompat.getDrawable(context, ic.iconId)!!
DrawableCompat.wrap(src).also { DrawableCompat.wrap(src).also {
DrawableCompat.setTint(it, ic.color) DrawableCompat.setTint(it, ContextCompat.getColor(context, ic.colorRes))
} }
} }
@ -190,7 +192,7 @@ class ActPushMessageList : AppCompatActivity() {
pm ?: return pm ?: return
lastItem = pm lastItem = pm
val iconAndColor = pm.notificationIconAndColor() val iconAndColor = pm.iconColor()
Glide.with(views.ivSmall) Glide.with(views.ivSmall)
.load(pm.iconSmall) .load(pm.iconSmall)
.error(tintIcon(iconAndColor)) .error(tintIcon(iconAndColor))

View File

@ -25,7 +25,6 @@ import jp.juggler.subwaytooter.column.startLoading
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.dialog.runInProgress import jp.juggler.subwaytooter.dialog.runInProgress
import jp.juggler.subwaytooter.notification.PushSubscriptionHelper
import jp.juggler.subwaytooter.notification.checkNotificationImmediate import jp.juggler.subwaytooter.notification.checkNotificationImmediate
import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll import jp.juggler.subwaytooter.notification.checkNotificationImmediateAll
import jp.juggler.subwaytooter.notification.recycleClickedNotification import jp.juggler.subwaytooter.notification.recycleClickedNotification
@ -47,7 +46,6 @@ import jp.juggler.util.queryIntentActivitiesCompat
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush import org.unifiedpush.android.connector.UnifiedPush
import java.util.ArrayList
private val log = LogCategory("ActMainIntent") private val log = LogCategory("ActMainIntent")
@ -351,7 +349,7 @@ fun ActMain.handleSharedIntent(intent: Intent) {
} }
// アカウントを追加/更新したらappServerHashの取得をやりなおす // アカウントを追加/更新したらappServerHashの取得をやりなおす
fun ActMain.updatePushDistributer(){ fun ActMain.updatePushDistributer() {
when { when {
fcmHandler.noFcm && prefDevice.pushDistributor.isNullOrEmpty() -> { fcmHandler.noFcm && prefDevice.pushDistributor.isNullOrEmpty() -> {
try { try {
@ -361,7 +359,7 @@ fun ActMain.updatePushDistributer(){
// 選択しなかった場合は購読の更新を行わない // 選択しなかった場合は購読の更新を行わない
} }
} }
else -> PushWorker.enqueueRegisterEndpoint(this) else -> PushWorker.enqueueRegisterEndpoint(this)
} }
} }

View File

@ -1,488 +1,488 @@
package jp.juggler.subwaytooter.notification package jp.juggler.subwaytooter.notification
import android.content.Context
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.SubscriptionServerKey
import jp.juggler.subwaytooter.table.appDatabase
import jp.juggler.util.*
import jp.juggler.util.data.*
import jp.juggler.util.log.*
import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.*
import okhttp3.Request
class PushSubscriptionHelper(
val context: Context,
val account: SavedAccount,
private val verbose: Boolean = false,
private val daoSubscriptionServerKey: SubscriptionServerKey.Access =
SubscriptionServerKey.Access(appDatabase),
) {
companion object {
private val log = LogCategory("PushSubscriptionHelper")
}
private val logBuffer = StringBuilder()
val logString: String
get() = logBuffer.toString()
private var subscribed: Boolean = false
val flags = account.notificationFlags()
private fun addLog(s: String?) {
if (s?.isNotEmpty() == true) {
if (logBuffer.isNotEmpty()) logBuffer.append('\n')
logBuffer.append(s)
}
}
// アプリサーバにendpoint URLの変更を伝える
private suspend fun registerEndpoint(
client: TootApiClient,
deviceId: String,
endpoint: String,
): TootApiResult {
// deprecated
// if (account.last_push_endpoint == endpoint) return TootApiResult()
return client.http(
buildJsonObject {
put("acct", account.acct.ascii)
put("deviceId", deviceId)
put("endpoint", endpoint)
}
.toPostRequestBuilder()
.url("$APP_SERVER/webpushendpoint")
.build()
).also { result ->
result.response?.let { res ->
when (res.code.also { res.close() }) {
in 200 until 300 -> {
// deprecated
// account.updateLastPushEndpoint(endpoint)
}
else -> {
result.caption = "(SubwayTooter App server)"
client.readBodyString(result)
}
}
}
}
}
// suspend fun updateSubscription(
// client: TootApiClient,
// force: Boolean = false,
// progress: suspend (SavedAccount, PollingState) -> Unit = { _, _ -> },
// ): TootApiResult? = try {
// when {
// account.isPseudo ->
// TootApiResult(context.getString(R.string.pseudo_account_not_supported))
// //
// else -> { //import android.content.Context
// progress(account, PollingState.CheckPushSubscription) //import jp.juggler.subwaytooter.R
// when { //import jp.juggler.subwaytooter.api.TootApiClient
// account.isMisskey -> updateSubscriptionMisskey(client) //import jp.juggler.subwaytooter.api.TootApiResult
// else -> updateSubscriptionMastodon(client, force) //import jp.juggler.subwaytooter.api.entity.*
//import jp.juggler.subwaytooter.table.SavedAccount
//import jp.juggler.subwaytooter.table.SubscriptionServerKey
//import jp.juggler.subwaytooter.table.appDatabase
//import jp.juggler.util.*
//import jp.juggler.util.data.*
//import jp.juggler.util.log.*
//import jp.juggler.util.network.toPostRequestBuilder
//import jp.juggler.util.ui.*
//import okhttp3.Request
//
//class PushSubscriptionHelper(
// val context: Context,
// val account: SavedAccount,
// private val verbose: Boolean = false,
// private val daoSubscriptionServerKey: SubscriptionServerKey.Access =
// SubscriptionServerKey.Access(appDatabase),
//) {
// companion object {
// private val log = LogCategory("PushSubscriptionHelper")
// }
//
// private val logBuffer = StringBuilder()
//
// val logString: String
// get() = logBuffer.toString()
//
// private var subscribed: Boolean = false
//
// val flags = account.notificationFlags()
//
// private fun addLog(s: String?) {
// if (s?.isNotEmpty() == true) {
// if (logBuffer.isNotEmpty()) logBuffer.append('\n')
// logBuffer.append(s)
// }
// }
//
// // アプリサーバにendpoint URLの変更を伝える
// private suspend fun registerEndpoint(
// client: TootApiClient,
// deviceId: String,
// endpoint: String,
// ): TootApiResult {
//
// // deprecated
// // if (account.last_push_endpoint == endpoint) return TootApiResult()
//
// return client.http(
// buildJsonObject {
// put("acct", account.acct.ascii)
// put("deviceId", deviceId)
// put("endpoint", endpoint)
// }
// .toPostRequestBuilder()
// .url("$APP_SERVER/webpushendpoint")
// .build()
// ).also { result ->
// result.response?.let { res ->
// when (res.code.also { res.close() }) {
// in 200 until 300 -> {
// // deprecated
// // account.updateLastPushEndpoint(endpoint)
// }
// else -> {
// result.caption = "(SubwayTooter App server)"
// client.readBodyString(result)
// }
// } // }
// } // }
// } // }
// } catch (ex: Throwable) { // }
// TootApiResult(ex.withCaption("error."))
// }?.apply {
// //
// if (error != null) addLog("$error $requestInfo".trimEnd()) //// suspend fun updateSubscription(
// //// client: TootApiClient,
// // update error text on account table //// force: Boolean = false,
// val log = logString //// progress: suspend (SavedAccount, PollingState) -> Unit = { _, _ -> },
// when { //// ): TootApiResult? = try {
// log.contains(ERROR_PREVENT_FREQUENTLY_CHECK) -> { //// when {
// // don't update if check was skipped. //// account.isPseudo ->
// } //// TootApiResult(context.getString(R.string.pseudo_account_not_supported))
// ////
// subscribed || log.isEmpty() -> Unit //// else -> {
// // clear error text if succeeded or no error log //// progress(account, PollingState.CheckPushSubscription)
//// if (account.last_subscription_error != null) { //// when {
//// account.updateSubscriptionError(null) //// account.isMisskey -> updateSubscriptionMisskey(client)
//// else -> updateSubscriptionMastodon(client, force)
//// } //// }
//// }
//// }
//// } catch (ex: Throwable) {
//// TootApiResult(ex.withCaption("error."))
//// }?.apply {
////
//// if (error != null) addLog("$error $requestInfo".trimEnd())
////
//// // update error text on account table
//// val log = logString
//// when {
//// log.contains(ERROR_PREVENT_FREQUENTLY_CHECK) -> {
//// // don't update if check was skipped.
//// }
////
//// subscribed || log.isEmpty() -> Unit
//// // clear error text if succeeded or no error log
////// if (account.last_subscription_error != null) {
////// account.updateSubscriptionError(null)
////// }
////
//// else -> Unit
//// // record error text
////// account.updateSubscriptionError(log)
//// }
//// }
// //
// else -> Unit //// private suspend fun updateSubscriptionMisskey(client: TootApiClient): TootApiResult? {
// // record error text ////
//// account.updateSubscriptionError(log) //// // 現在の購読状態を取得できないので、毎回購読の更新を行う
// } //// // FCMのデバイスIDを取得
// } //// val deviceId = try {
//// loadFirebaseMessagingToken(context)
// private suspend fun updateSubscriptionMisskey(client: TootApiClient): TootApiResult? { //// } catch (ex: Throwable) {
//// log.e(ex, "loadFirebaseMessagingToken failed.")
//// return when (ex) {
//// is CancellationException -> null
//// else -> TootApiResult(error = context.getString(R.string.missing_fcm_device_id))
//// }
//// }
////
//// // アクセストークン
//// val accessToken = account.misskeyApiToken
//// ?: return TootApiResult(error = "missing misskeyApiToken.")
////
//// // インストールIDを取得
//// val installId = try {
//// loadInstallId(
//// context,
//// account,
//// deviceId
//// ) { a, s -> log.i("[${a.acct.pretty}]${s.desc}") }
//// } catch (ex: Throwable) {
//// log.e(ex, "loadInstallId failed.")
//// return when (ex) {
//// is CancellationException -> null
//// else -> TootApiResult(error = context.getString(R.string.missing_install_id))
//// }
//// }
////
//// // クライアント識別子
//// val clientIdentifier = "$accessToken$installId".digestSHA256Base64Url()
////
//// // 購読が不要な場合
//// // アプリサーバが410を返せるように状態を通知する
//// if (flags == 0) return registerEndpoint(client, deviceId, "none").also {
//// if (it.error == null && verbose) addLog(context.getString(R.string.push_subscription_updated))
//// }
////
//// /*
//// https://github.com/syuilo/misskey/blob/master/src/services/create-notification.ts#L46
//// Misskeyは通知に既読の概念があり、イベント発生後2秒たっても未読の時だけプッシュ通知が発生する。
//// STでプッシュ通知を試すにはSTの画面を非表示にする必要があるのでWebUIを使って投稿していたが、
//// WebUIを開いていると通知はすぐ既読になるのでプッシュ通知は発生しない。
//// プッシュ通知のテスト時はST2台を使い、片方をプッシュ通知の受信チェック、もう片方を投稿などの作業に使うことになる。
//// */
////
//// // https://github.com/syuilo/misskey/issues/2541
//// // https://github.com/syuilo/misskey/commit/4c6fb60dd25d7e2865fc7c4d97728593ffc3c902
//// // 2018/9/1 の上記コミット以降、Misskeyでもサーバ公開鍵を得られるようになった
////
//// val endpoint =
//// "$APP_SERVER/webpushcallback/${deviceId.encodePercent()}/${account.acct.ascii.encodePercent()}/$flags/$clientIdentifier/misskey"
////
//// // アプリサーバが過去のendpoint urlに410を返せるよう、状態を通知する
//// val r = registerEndpoint(client, deviceId, endpoint.toUri().encodedPath!!)
//// if (r.error != null) return r
////
//// // 購読
//// @Suppress("SpellCheckingInspection")
//// return client.request(
//// "/api/sw/register",
//// account.putMisskeyApiToken().apply {
//// put("endpoint", endpoint)
//// put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
//// put(
//// "publickey",
//// "BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE"
//// )
//// }
//// .toPostRequestBuilder()
//// )?.also { result ->
//// val jsonObject = result.jsonObject
//// if (jsonObject == null) {
//// addLog("API error.")
//// } else {
//// if (verbose) addLog(context.getString(R.string.push_subscription_updated))
//// subscribed = true
//// return updateServerKey(
//// client,
//// clientIdentifier,
//// jsonObject.string("key") ?: "3q2+rw"
//// )
//// }
//// }
//// }
// //
// // 現在の購読状態を取得できないので、毎回購読の更新を行う //// private suspend fun updateSubscriptionMastodon(
// // FCMのデバイスIDを取得 //// client: TootApiClient,
// val deviceId = try { //// force: Boolean,
// loadFirebaseMessagingToken(context) //// ): TootApiResult? {
// } catch (ex: Throwable) { ////
// log.e(ex, "loadFirebaseMessagingToken failed.") //// // 現在の購読状態を取得
// return when (ex) { //// // https://github.com/tootsuite/mastodon/pull/7471
// is CancellationException -> null //// // https://github.com/tootsuite/mastodon/pull/7472
// else -> TootApiResult(error = context.getString(R.string.missing_fcm_device_id)) ////
// } //// val subscription404: Boolean
// } //// val oldSubscription: TootPushSubscription?
//// checkCurrentSubscription(client).let {
//// if (it.failed) return it.result
//// subscription404 = it.is404
//// oldSubscription = parseItem(::TootPushSubscription, it.result?.jsonObject)
//// }
////
//// if (oldSubscription == null) {
//// log.i("${account.acct}: oldSubscription is null")
//// val (ti, result) = TootInstance.get(client)
//// ti ?: return result
//// checkInstanceVersionMastodon(ti, subscription404)?.let { return it }
//// }
////
//// // FCMのデバイスIDを取得
//// val deviceId = try {
//// loadFirebaseMessagingToken(context)
//// } catch (ex: Throwable) {
//// log.e(ex, "loadFirebaseMessagingToken failed.")
//// return when (ex) {
//// is CancellationException -> null
//// else -> TootApiResult(error = context.getString(R.string.missing_fcm_device_id))
//// }
//// }
////
//// // インストールIDを取得
//// val installId = try {
//// loadInstallId(
//// context,
//// account,
//// deviceId
//// ) { a, s -> log.i("[${a.acct.pretty}]${s.desc}") }
//// } catch (ex: Throwable) {
//// log.e(ex, "loadInstallId failed.")
//// return when (ex) {
//// is CancellationException -> null
//// else -> TootApiResult(error = context.getString(R.string.missing_install_id))
//// }
//// }
//// // アクセストークン
//// val accessToken = account.bearerAccessToken
//// ?: return TootApiResult(error = "missing access token.")
////
//// // アクセストークンのダイジェスト
//// val tokenDigest = accessToken.digestSHA256Base64Url()
////
//// // クライアント識別子
//// val clientIdentifier = "$accessToken$installId".digestSHA256Base64Url()
////
//// val endpoint =
//// "$APP_SERVER/webpushcallback/${deviceId.encodePercent()}/${account.acct.ascii.encodePercent()}/$flags/$clientIdentifier"
////
//// val newAlerts = JsonObject().apply {
//// put("follow", account.notification_follow)
//// put(TootNotification.TYPE_ADMIN_SIGNUP, account.notification_follow)
//// put("favourite", account.notification_favourite)
//// put("reblog", account.notification_boost)
//// put("mention", account.notification_mention)
//// put("poll", account.notification_vote)
//// put("follow_request", account.notification_follow_request)
//// put("status", account.notification_post)
//// put("update", account.notification_update)
//// put("emoji_reaction", account.notification_reaction) // fedibird拡張
//// }
////
//// if (!force) {
//// canSkipSubscriptionMastodon(
//// client = client,
//// clientIdentifier = clientIdentifier,
//// endpoint = endpoint,
//// oldSubscription = oldSubscription,
//// newAlerts = newAlerts,
//// )?.let { return it }
//// }
////
//// // アクセストークンの優先権を取得
//// checkDeviceHasPriority(
//// client,
//// tokenDigest = tokenDigest,
//// installId = installId,
//// ).let {
//// if (it.failed) return it.result
//// }
////
//// return when (flags) {
//// // 通知設定が全てカラなので、購読を取り消したい
//// 0 -> unsubscribeMastodon(client)
////
//// // 通知設定が空ではないので購読を行いたい
//// else -> subscribeMastodon(
//// client = client,
//// clientIdentifier = clientIdentifier,
//// endpoint = endpoint,
//// newAlerts = newAlerts
//// )
//// }
//// }
// //
// // アクセストークン // // returns null if no error
// val accessToken = account.misskeyApiToken // private fun checkInstanceVersionMastodon(
// ?: return TootApiResult(error = "missing misskeyApiToken.") // ti: TootInstance,
// // subscription404: Boolean,
// // インストールIDを取得
// val installId = try {
// loadInstallId(
// context,
// account,
// deviceId
// ) { a, s -> log.i("[${a.acct.pretty}]${s.desc}") }
// } catch (ex: Throwable) {
// log.e(ex, "loadInstallId failed.")
// return when (ex) {
// is CancellationException -> null
// else -> TootApiResult(error = context.getString(R.string.missing_install_id))
// }
// }
//
// // クライアント識別子
// val clientIdentifier = "$accessToken$installId".digestSHA256Base64Url()
//
// // 購読が不要な場合
// // アプリサーバが410を返せるように状態を通知する
// if (flags == 0) return registerEndpoint(client, deviceId, "none").also {
// if (it.error == null && verbose) addLog(context.getString(R.string.push_subscription_updated))
// }
//
// /*
// https://github.com/syuilo/misskey/blob/master/src/services/create-notification.ts#L46
// Misskeyは通知に既読の概念があり、イベント発生後2秒たっても未読の時だけプッシュ通知が発生する。
// STでプッシュ通知を試すにはSTの画面を非表示にする必要があるのでWebUIを使って投稿していたが、
// WebUIを開いていると通知はすぐ既読になるのでプッシュ通知は発生しない。
// プッシュ通知のテスト時はST2台を使い、片方をプッシュ通知の受信チェック、もう片方を投稿などの作業に使うことになる。
// */
//
// // https://github.com/syuilo/misskey/issues/2541
// // https://github.com/syuilo/misskey/commit/4c6fb60dd25d7e2865fc7c4d97728593ffc3c902
// // 2018/9/1 の上記コミット以降、Misskeyでもサーバ公開鍵を得られるようになった
//
// val endpoint =
// "$APP_SERVER/webpushcallback/${deviceId.encodePercent()}/${account.acct.ascii.encodePercent()}/$flags/$clientIdentifier/misskey"
//
// // アプリサーバが過去のendpoint urlに410を返せるよう、状態を通知する
// val r = registerEndpoint(client, deviceId, endpoint.toUri().encodedPath!!)
// if (r.error != null) return r
//
// // 購読
// @Suppress("SpellCheckingInspection")
// return client.request(
// "/api/sw/register",
// account.putMisskeyApiToken().apply {
// put("endpoint", endpoint)
// put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
// put(
// "publickey",
// "BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE"
// )
// }
// .toPostRequestBuilder()
// )?.also { result ->
// val jsonObject = result.jsonObject
// if (jsonObject == null) {
// addLog("API error.")
// } else {
// if (verbose) addLog(context.getString(R.string.push_subscription_updated))
// subscribed = true
// return updateServerKey(
// client,
// clientIdentifier,
// jsonObject.string("key") ?: "3q2+rw"
// )
// }
// }
// }
// private suspend fun updateSubscriptionMastodon(
// client: TootApiClient,
// force: Boolean,
// ): TootApiResult? { // ): TootApiResult? {
// //
// // 現在の購読状態を取得 // // 2.4.0rc1 未満にはプッシュ購読APIはない
// // https://github.com/tootsuite/mastodon/pull/7471 // if (!ti.versionGE(TootInstance.VERSION_2_4_0_rc1)) {
// // https://github.com/tootsuite/mastodon/pull/7472 // return TootApiResult(
// // context.getString(R.string.instance_does_not_support_push_api, ti.version)
// val subscription404: Boolean
// val oldSubscription: TootPushSubscription?
// checkCurrentSubscription(client).let {
// if (it.failed) return it.result
// subscription404 = it.is404
// oldSubscription = parseItem(::TootPushSubscription, it.result?.jsonObject)
// }
//
// if (oldSubscription == null) {
// log.i("${account.acct}: oldSubscription is null")
// val (ti, result) = TootInstance.get(client)
// ti ?: return result
// checkInstanceVersionMastodon(ti, subscription404)?.let { return it }
// }
//
// // FCMのデバイスIDを取得
// val deviceId = try {
// loadFirebaseMessagingToken(context)
// } catch (ex: Throwable) {
// log.e(ex, "loadFirebaseMessagingToken failed.")
// return when (ex) {
// is CancellationException -> null
// else -> TootApiResult(error = context.getString(R.string.missing_fcm_device_id))
// }
// }
//
// // インストールIDを取得
// val installId = try {
// loadInstallId(
// context,
// account,
// deviceId
// ) { a, s -> log.i("[${a.acct.pretty}]${s.desc}") }
// } catch (ex: Throwable) {
// log.e(ex, "loadInstallId failed.")
// return when (ex) {
// is CancellationException -> null
// else -> TootApiResult(error = context.getString(R.string.missing_install_id))
// }
// }
// // アクセストークン
// val accessToken = account.bearerAccessToken
// ?: return TootApiResult(error = "missing access token.")
//
// // アクセストークンのダイジェスト
// val tokenDigest = accessToken.digestSHA256Base64Url()
//
// // クライアント識別子
// val clientIdentifier = "$accessToken$installId".digestSHA256Base64Url()
//
// val endpoint =
// "$APP_SERVER/webpushcallback/${deviceId.encodePercent()}/${account.acct.ascii.encodePercent()}/$flags/$clientIdentifier"
//
// val newAlerts = JsonObject().apply {
// put("follow", account.notification_follow)
// put(TootNotification.TYPE_ADMIN_SIGNUP, account.notification_follow)
// put("favourite", account.notification_favourite)
// put("reblog", account.notification_boost)
// put("mention", account.notification_mention)
// put("poll", account.notification_vote)
// put("follow_request", account.notification_follow_request)
// put("status", account.notification_post)
// put("update", account.notification_update)
// put("emoji_reaction", account.notification_reaction) // fedibird拡張
// }
//
// if (!force) {
// canSkipSubscriptionMastodon(
// client = client,
// clientIdentifier = clientIdentifier,
// endpoint = endpoint,
// oldSubscription = oldSubscription,
// newAlerts = newAlerts,
// )?.let { return it }
// }
//
// // アクセストークンの優先権を取得
// checkDeviceHasPriority(
// client,
// tokenDigest = tokenDigest,
// installId = installId,
// ).let {
// if (it.failed) return it.result
// }
//
// return when (flags) {
// // 通知設定が全てカラなので、購読を取り消したい
// 0 -> unsubscribeMastodon(client)
//
// // 通知設定が空ではないので購読を行いたい
// else -> subscribeMastodon(
// client = client,
// clientIdentifier = clientIdentifier,
// endpoint = endpoint,
// newAlerts = newAlerts
// ) // )
// } // }
//
// if (subscription404 && flags == 0) {
// when {
// ti.versionGE(TootInstance.VERSION_2_4_0_rc2) -> {
// // 購読が不要で現在の状況が404だった場合
// // 2.4.0rc2以降では「購読が存在しない」を示すので何もしなくてよい
// if (verbose) addLog(context.getString(R.string.push_subscription_not_exists))
// return TootApiResult()
// }
//
// else -> {
// // 2.4.0rc1では「APIが存在しない」と「購読が存在しない」を判別できない
// }
// }
// }
// return null
// } // }
//
// returns null if no error // private class CheckCurrentSubscriptionResult(
private fun checkInstanceVersionMastodon( // val result: TootApiResult?,
ti: TootInstance, // val failed: Boolean,
subscription404: Boolean, // val is404: Boolean,
): TootApiResult? { // )
//
// 2.4.0rc1 未満にはプッシュ購読APIはない // @Suppress("BooleanLiteralArgument")
if (!ti.versionGE(TootInstance.VERSION_2_4_0_rc1)) { // private suspend fun checkCurrentSubscription(client: TootApiClient): CheckCurrentSubscriptionResult {
return TootApiResult( // val r = client.request("/api/v1/push/subscription")
context.getString(R.string.instance_does_not_support_push_api, ti.version) // fun rvError() = CheckCurrentSubscriptionResult(r, true, false)
) // fun rvOk() = CheckCurrentSubscriptionResult(r, false, false)
} // fun rv404() = CheckCurrentSubscriptionResult(r, false, true)
// val res = r?.response ?: return rvError() // cancelled or missing response
if (subscription404 && flags == 0) { //
when { // if (res.code != 200) log.i("${account.acct}: check existing subscription: code=${res.code}")
ti.versionGE(TootInstance.VERSION_2_4_0_rc2) -> { //
// 購読が不要で現在の状況が404だった場合 // return when (res.code) {
// 2.4.0rc2以降では「購読が存在しない」を示すので何もしなくてよい // 200 -> {
if (verbose) addLog(context.getString(R.string.push_subscription_not_exists)) // if (r.error?.isNotEmpty() == true && r.jsonObject == null) {
return TootApiResult() // // Pleromaが200応答でもエラーHTMLを返す場合がある
} // addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma))
// rvError()
else -> { // } else {
// 2.4.0rc1では「APIが存在しない」と「購読が存在しない」を判別できない // // たぶん購読が存在する
} // rvOk()
} // }
} // }
return null //
} // // この時点では存在しないのが購読なのかAPIなのか分からない
// 404 -> rv404()
private class CheckCurrentSubscriptionResult( //
val result: TootApiResult?, // 403 -> {
val failed: Boolean, // // アクセストークンにpushスコープがない
val is404: Boolean, // if (flags != 0 || verbose) addLog(context.getString(R.string.missing_push_scope))
) // rvError()
// }
@Suppress("BooleanLiteralArgument") //
private suspend fun checkCurrentSubscription(client: TootApiClient): CheckCurrentSubscriptionResult { // in 400 until 500 -> {
val r = client.request("/api/v1/push/subscription") // addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma))
fun rvError() = CheckCurrentSubscriptionResult(r, true, false) // rvError()
fun rvOk() = CheckCurrentSubscriptionResult(r, false, false) // }
fun rv404() = CheckCurrentSubscriptionResult(r, false, true) //
val res = r?.response ?: return rvError() // cancelled or missing response // else -> {
// addLog("${res.request}")
if (res.code != 200) log.i("${account.acct}: check existing subscription: code=${res.code}") // addLog("${res.code} ${res.message}")
// rvOk() // 後でリトライする
return when (res.code) { // }
200 -> { // }
if (r.error?.isNotEmpty() == true && r.jsonObject == null) { // }
// Pleromaが200応答でもエラーHTMLを返す場合がある //
addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma)) // private suspend fun unsubscribeMastodon(
rvError() // client: TootApiClient,
} else { // ): TootApiResult? {
// たぶん購読が存在する //
rvOk() // val r = client.request("/api/v1/push/subscription", Request.Builder().delete())
} // val res = r?.response ?: return r
} //
// return when (res.code) {
// この時点では存在しないのが購読なのかAPIなのか分からない // 200 -> {
404 -> rv404() // if (verbose) addLog(context.getString(R.string.push_subscription_deleted))
// TootApiResult()
403 -> { // }
// アクセストークンにpushスコープがない //
if (flags != 0 || verbose) addLog(context.getString(R.string.missing_push_scope)) // 404 -> {
rvError() // if (verbose) {
} // addLog(context.getString(R.string.missing_push_api))
// r
in 400 until 500 -> { // } else {
addLog(context.getString(R.string.instance_does_not_support_push_api_pleroma)) // TootApiResult()
rvError() // }
} // }
//
else -> { // 403 -> {
addLog("${res.request}") // addLog(context.getString(R.string.missing_push_scope))
addLog("${res.code} ${res.message}") // r
rvOk() // 後でリトライする // }
} //
} // else -> {
} // addLog("${res.request}")
// addLog("${res.code} ${res.message}")
private suspend fun unsubscribeMastodon( // r
client: TootApiClient, // }
): TootApiResult? { // }
// }
val r = client.request("/api/v1/push/subscription", Request.Builder().delete()) //
val res = r?.response ?: return r // private suspend fun subscribeMastodon(
// client: TootApiClient,
return when (res.code) { // endpoint: String,
200 -> { // newAlerts: JsonObject,
if (verbose) addLog(context.getString(R.string.push_subscription_deleted)) // ): TootApiResult? {
TootApiResult() // @Suppress("SpellCheckingInspection")
} // val params = JsonObject().apply {
// put("subscription", JsonObject().apply {
404 -> { // put("endpoint", endpoint)
if (verbose) { // put("keys", JsonObject().apply {
addLog(context.getString(R.string.missing_push_api)) // put(
r // "p256dh",
} else { // "BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE"
TootApiResult() // )
} // put("auth", "iRdmDrOS6eK6xvG1H6KshQ")
} // })
// })
403 -> { // put("data", JsonObject().apply {
addLog(context.getString(R.string.missing_push_scope)) // put("alerts", newAlerts)
r // account.pushPolicy?.let { put("policy", it) }
} // })
// }
else -> { //
addLog("${res.request}") // val r = client.request(
addLog("${res.code} ${res.message}") // "/api/v1/push/subscription",
r // params.toPostRequestBuilder()
} // ) ?: return null
} //
} // val res = r.response ?: return r
//
private suspend fun subscribeMastodon( // return when (res.code) {
client: TootApiClient, // 404 -> {
endpoint: String, // addLog(context.getString(R.string.missing_push_api))
newAlerts: JsonObject, // r
): TootApiResult? { // }
@Suppress("SpellCheckingInspection") //
val params = JsonObject().apply { // 403 -> {
put("subscription", JsonObject().apply { // addLog(context.getString(R.string.missing_push_scope))
put("endpoint", endpoint) // r
put("keys", JsonObject().apply { // }
put( //
"p256dh", // 200 -> {
"BBEUVi7Ehdzzpe_ZvlzzkQnhujNJuBKH1R0xYg7XdAKNFKQG9Gpm0TSGRGSuaU7LUFKX-uz8YW0hAshifDCkPuE" // subscribed = true
) // if (verbose) addLog(context.getString(R.string.push_subscription_updated))
put("auth", "iRdmDrOS6eK6xvG1H6KshQ") // return TootApiResult()
}) // }
}) //
put("data", JsonObject().apply { // else -> {
put("alerts", newAlerts) // addLog(r.jsonObject?.toString())
account.pushPolicy?.let { put("policy", it) } // r
}) // }
} // }
// }
val r = client.request( //}
"/api/v1/push/subscription",
params.toPostRequestBuilder()
) ?: return null
val res = r.response ?: return r
return when (res.code) {
404 -> {
addLog(context.getString(R.string.missing_push_api))
r
}
403 -> {
addLog(context.getString(R.string.missing_push_scope))
r
}
200 -> {
subscribed = true
if (verbose) addLog(context.getString(R.string.push_subscription_updated))
return TootApiResult()
}
else -> {
addLog(r.jsonObject?.toString())
r
}
}
}
}

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.notification package jp.juggler.subwaytooter.push
import android.graphics.Color import androidx.annotation.ColorRes
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.table.PushMessage import jp.juggler.subwaytooter.table.PushMessage
@ -9,86 +8,84 @@ import jp.juggler.util.log.LogCategory
private val log = LogCategory("NotificationIconAndColor") private val log = LogCategory("NotificationIconAndColor")
enum class NotificationIconAndColor( enum class PushMessageIconColor(
@ColorInt colorArg: Int, @ColorRes val colorRes: Int,
@DrawableRes val iconId: Int, @DrawableRes val iconId: Int,
val keys: Array<String>, val keys: Array<String>,
) { ) {
Favourite( Favourite(
0xe5e825, R.color.colorNotificationAccentFavourite,
R.drawable.ic_star_outline, R.drawable.ic_star_outline,
arrayOf("favourite"), arrayOf("favourite"),
), ),
Mention( Mention(
0x60f516, R.color.colorNotificationAccentMention,
R.drawable.outline_alternate_email_24, R.drawable.outline_alternate_email_24,
arrayOf("mention"), arrayOf("mention"),
), ),
Reply( Reply(
0xff3dbb, R.color.colorNotificationAccentReply,
R.drawable.ic_reply, R.drawable.ic_reply,
arrayOf("reply") arrayOf("reply")
), ),
Reblog( Reblog(
0x39e3d5, R.color.colorNotificationAccentReblog,
R.drawable.ic_repeat, R.drawable.ic_repeat,
arrayOf("reblog", "renote"), arrayOf("reblog", "renote"),
), ),
Quote( Quote(
0x40a9ff, R.color.colorNotificationAccentQuote,
R.drawable.ic_quote, R.drawable.ic_quote,
arrayOf("quote"), arrayOf("quote"),
), ),
Follow( Follow(
0xf57a33, R.color.colorNotificationAccentFollow,
R.drawable.ic_person_add, R.drawable.ic_person_add,
arrayOf("follow", "followRequestAccepted") arrayOf("follow", "followRequestAccepted")
), ),
Unfollow( Unfollow(
0x9433f5, R.color.colorNotificationAccentUnfollow,
R.drawable.ic_follow_cross, R.drawable.ic_follow_cross,
arrayOf("unfollow") arrayOf("unfollow")
), ),
Reaction( Reaction(
0xf5f233, R.color.colorNotificationAccentReaction,
R.drawable.outline_add_reaction_24, R.drawable.outline_add_reaction_24,
arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction") arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction")
), ),
FollowRequest( FollowRequest(
0xf53333, R.color.colorNotificationAccentFollowRequest,
R.drawable.ic_follow_wait, R.drawable.ic_follow_wait,
arrayOf("follow_request", "receiveFollowRequest"), arrayOf("follow_request", "receiveFollowRequest"),
), ),
Poll( Poll(
0x33f59b, R.color.colorNotificationAccentPoll,
R.drawable.outline_poll_24, R.drawable.outline_poll_24,
arrayOf("pollVote", "poll_vote", "poll"), arrayOf("pollVote", "poll_vote", "poll"),
), ),
Status( Status(
0x33f597, R.color.colorNotificationAccentStatus,
R.drawable.ic_edit, R.drawable.ic_edit,
arrayOf("status", "update", "status_reference") arrayOf("status", "update", "status_reference")
), ),
SignUp( SignUp(
0xf56a33, R.color.colorNotificationAccentSignUp,
R.drawable.outline_group_add_24, R.drawable.outline_group_add_24,
arrayOf("admin.sign_up"), arrayOf("admin.sign_up"),
), ),
Unknown( Unknown(
0xae1aed, R.color.colorNotificationAccentUnknown,
R.drawable.ic_question, R.drawable.ic_question,
arrayOf("unknown"), arrayOf("unknown", "admin.sign_up"),
) )
; ;
val color = Color.BLACK or colorArg
companion object { companion object {
val map = buildMap { val map = buildMap {
values().forEach { values().forEach {
for (k in it.keys) { for (k in it.keys) {
val old: NotificationIconAndColor? = get(k) val old: PushMessageIconColor? = get(k)
if (old != null) { if (old != null) {
error("NotificationIconAndColor: $k is duplicate: ${it.name} and ${old.name}") error("NotificationIconAndColor: $k is duplicate: ${it.name} and ${old.name}")
} else { } else {
@ -100,20 +97,6 @@ enum class NotificationIconAndColor(
} }
} }
fun String.findNotificationIconAndColor() = fun PushMessage.iconColor() =
NotificationIconAndColor.map[this] notificationType?.let { PushMessageIconColor.map[it] }
?: PushMessageIconColor.Unknown
fun PushMessage.notificationIconAndColor(): NotificationIconAndColor {
// mastodon
messageJson?.string("notification_type")
?.findNotificationIconAndColor()?.let { return it }
// misskey
when (messageJson?.string("type")) {
"notification" ->
messageJson?.jsonObject("body")?.string("type")
?.findNotificationIconAndColor()?.let { return it }
}
return NotificationIconAndColor.Unknown
}

View File

@ -4,11 +4,11 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.await import androidx.work.await
import jp.juggler.anko.BuildConfig
import jp.juggler.crypt.* import jp.juggler.crypt.*
import jp.juggler.subwaytooter.ActCallback import jp.juggler.subwaytooter.ActCallback
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
@ -20,7 +20,7 @@ import jp.juggler.subwaytooter.api.push.ApiPushMisskey
import jp.juggler.subwaytooter.dialog.SuspendProgress import jp.juggler.subwaytooter.dialog.SuspendProgress
import jp.juggler.subwaytooter.notification.NotificationChannels import jp.juggler.subwaytooter.notification.NotificationChannels
import jp.juggler.subwaytooter.notification.NotificationDeleteReceiver.Companion.intentNotificationDelete import jp.juggler.subwaytooter.notification.NotificationDeleteReceiver.Companion.intentNotificationDelete
import jp.juggler.subwaytooter.notification.notificationIconAndColor import jp.juggler.subwaytooter.notification.iconColor
import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.pref.prefDevice import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.subwaytooter.push.* import jp.juggler.subwaytooter.push.*
@ -637,7 +637,7 @@ class PushRepo(
} }
val density = context.resources.displayMetrics.density val density = context.resources.displayMetrics.density
val iconAndColor = pm.notificationIconAndColor() val iconAndColor = pm.iconColor()
suspend fun PushMessage.loadSmallIcon(context: Context): IconCompat { suspend fun PushMessage.loadSmallIcon(context: Context): IconCompat {
iconSmall?.notEmpty() iconSmall?.notEmpty()
@ -687,7 +687,7 @@ class PushRepo(
// val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE) // val piTap = PendingIntent.getActivity(this, nc.pircTap, iTap, PendingIntent.FLAG_IMMUTABLE)
ncPushMessage.notify(context, urlDelete) { ncPushMessage.notify(context, urlDelete) {
color = iconAndColor.color color = ContextCompat.getColor(context,iconAndColor.colorRes)
setSmallIcon(iconSmall) setSmallIcon(iconSmall)
iconBitmapLarge?.let { setLargeIcon(it) } iconBitmapLarge?.let { setLargeIcon(it) }
setContentTitle(pm.loginAcct) setContentTitle(pm.loginAcct)

View File

@ -156,4 +156,20 @@
<!-- 白テーマのナビゲーションバーは白ではない --> <!-- 白テーマのナビゲーションバーは白ではない -->
<color name="colorNavigationBarWorkaround">#707070</color> <color name="colorNavigationBarWorkaround">#707070</color>
<!-- 通知のアクセント色 Pushメッセージ用 -->
<color name="colorNotificationAccentFavourite">#e5e825</color>
<color name="colorNotificationAccentFollow">#f57a33</color>
<color name="colorNotificationAccentFollowRequest">#f53333</color>
<color name="colorNotificationAccentMention">#60f516</color>
<color name="colorNotificationAccentPoll">#33f59b</color>
<color name="colorNotificationAccentQuote">#40a9ff</color>
<color name="colorNotificationAccentReaction">#f5f233</color>
<color name="colorNotificationAccentReblog">#39e3d5</color>
<color name="colorNotificationAccentReply">#ff3dbb</color>
<color name="colorNotificationAccentSignUp">#f56a33</color>
<color name="colorNotificationAccentStatus">#33f597</color>
<color name="colorNotificationAccentUnfollow">#9433f5</color>
<color name="colorNotificationAccentUnknown">#ae1aed</color>
</resources> </resources>