- remove ForegroundPollingService
- check notifications in  MyFirebaseMessagingService.onMessageReceived
- dont send empty search request.

Dependencies:
- coreLibraryDesugaringEnabled true
- desugar_jdk_libs:1.2.0
- material:1.7.0
- exifinterface:1.3.5
- annotation:1.5.0
- firebase-messaging:23.1.0
- work-runtime-ktx:2.8.0-beta01
- appcompat_version = 1.5.1
- kotlin_version = 1.7.20
- Android Gradle plugin 7.3.1
- google-services:4.3.14
This commit is contained in:
tateisu 2022-11-07 11:45:17 +09:00
parent 0a73842052
commit 19d626e5f1
9 changed files with 216 additions and 167 deletions

View File

@ -19,6 +19,7 @@ android {
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
} }
defaultConfig { defaultConfig {
@ -128,6 +129,10 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation fileTree(include: ['*.aar'], dir: 'src/main/libs') implementation fileTree(include: ['*.aar'], dir: 'src/main/libs')
// desugar_jdk_libs 2.0.0 AGP 7.4.0-alpha10
//noinspection GradleDependency
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.0'
// targetSdkVersion 31 androidTest android:exported // targetSdkVersion 31 androidTest android:exported
// https://github.com/android/android-test/issues/1022 // https://github.com/android/android-test/issues/1022
androidTestImplementation "androidx.test:core:1.4.0" androidTestImplementation "androidx.test:core:1.4.0"
@ -158,9 +163,9 @@ dependencies {
implementation "androidx.drawerlayout:drawerlayout:1.1.1" implementation "androidx.drawerlayout:drawerlayout:1.1.1"
// NavigationView // NavigationView
implementation "com.google.android.material:material:1.6.1" implementation "com.google.android.material:material:1.7.0"
implementation "androidx.exifinterface:exifinterface:1.3.3" implementation "androidx.exifinterface:exifinterface:1.3.5"
// CustomTabs // CustomTabs
implementation "androidx.browser:browser:1.4.0" implementation "androidx.browser:browser:1.4.0"
@ -168,10 +173,10 @@ dependencies {
// Recyclerview // Recyclerview
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
kapt 'androidx.annotation:annotation:1.4.0' kapt 'androidx.annotation:annotation:1.5.0'
// https://firebase.google.com/support/release-notes/android // https://firebase.google.com/support/release-notes/android
implementation "com.google.firebase:firebase-messaging:23.0.8" implementation "com.google.firebase:firebase-messaging:23.1.0"
implementation "org.jetbrains.kotlin:kotlin-reflect" implementation "org.jetbrains.kotlin:kotlin-reflect"
testImplementation "org.jetbrains.kotlin:kotlin-test" testImplementation "org.jetbrains.kotlin:kotlin-test"
@ -262,7 +267,7 @@ dependencies {
// optional - Test helpers for LiveData // optional - Test helpers for LiveData
testImplementation "androidx.arch.core:core-testing:$arch_version" testImplementation "androidx.arch.core:core-testing:$arch_version"
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.8.0-beta01'
def roomVersion = "2.4.3" def roomVersion = "2.4.3"
implementation "androidx.room:room-runtime:$roomVersion" implementation "androidx.room:room-runtime:$roomVersion"

View File

@ -354,9 +354,6 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
</service> </service>
<service
android:name="jp.juggler.subwaytooter.notification.ForegroundPollingService"
android:exported="false" />
<provider <provider
android:name="androidx.startup.InitializationProvider" android:name="androidx.startup.InitializationProvider"

View File

@ -2,12 +2,13 @@ package jp.juggler.subwaytooter
import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage import com.google.firebase.messaging.RemoteMessage
import jp.juggler.subwaytooter.notification.ForegroundPollingService import jp.juggler.subwaytooter.notification.PollingChecker
import jp.juggler.subwaytooter.notification.restartAllWorker import jp.juggler.subwaytooter.notification.restartAllWorker
import jp.juggler.subwaytooter.pref.PrefDevice import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.table.NotificationCache import jp.juggler.subwaytooter.table.NotificationCache
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.LogCategory import jp.juggler.util.LogCategory
import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
class MyFirebaseMessagingService : FirebaseMessagingService() { class MyFirebaseMessagingService : FirebaseMessagingService() {
@ -33,9 +34,22 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
} }
} }
override fun onNewToken(token: String) {
try {
log.w("onTokenRefresh: token=$token")
PrefDevice.from(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply()
restartAllWorker(this)
} catch (ex: Throwable) {
log.trace(ex, "onNewToken failed")
}
}
override fun onMessageReceived(remoteMessage: RemoteMessage) { override fun onMessageReceived(remoteMessage: RemoteMessage) {
val context = this val context = this
val messageId = remoteMessage.messageId ?: return
if (isDuplicateMessage(messageId)) return
val accounts = ArrayList<SavedAccount>() val accounts = ArrayList<SavedAccount>()
for ((key, value) in remoteMessage.data) { for ((key, value) in remoteMessage.data) {
log.w("onMessageReceived: $key=$value") log.w("onMessageReceived: $key=$value")
@ -60,19 +74,26 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
NotificationCache.resetLastLoad() NotificationCache.resetLastLoad()
accounts.addAll(SavedAccount.loadAccountList(context)) accounts.addAll(SavedAccount.loadAccountList(context))
} }
log.i("accounts.size=${accounts.size}")
accounts.forEach { log.i("accounts.size=${accounts.size} thred=${Thread.currentThread().name}")
ForegroundPollingService.start(this, remoteMessage.messageId, it.db_id) runBlocking {
accounts.forEach {
check(it.db_id)
}
} }
} }
override fun onNewToken(token: String) { private suspend fun check(accountDbId: Long) {
try { try {
log.w("onTokenRefresh: token=$token") PollingChecker(
PrefDevice.from(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply() context = this,
restartAllWorker(this) accountDbId = accountDbId
).check { a, s ->
val text = "[${a.acct.pretty}]${s.desc}"
log.i(text)
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.trace(ex, "onNewToken failed") log.trace(ex)
} }
} }
} }

View File

@ -1034,7 +1034,7 @@ class TootApiClient(
if (result.error != null) return result if (result.error != null) return result
val instance = result.caption // same to instance val instance = result.caption // same to instance
val clientName = if (clientNameArg.isNotEmpty()) clientNameArg else DEFAULT_CLIENT_NAME val clientName = clientNameArg.ifEmpty { DEFAULT_CLIENT_NAME }
val clientInfo = val clientInfo =
ClientInfo.load(instance, clientName) ?: return result.setError("missing client id") ClientInfo.load(instance, clientName) ?: return result.setError("missing client id")
@ -1271,12 +1271,24 @@ class TootApiClient(
} }
} }
// query: query_string after ? ( ? itself is excluded )
suspend fun TootApiClient.requestMastodonSearch( suspend fun TootApiClient.requestMastodonSearch(
parser: TootParser, parser: TootParser,
query: String, // 検索文字列
q: String,
// リモートサーバの情報を解決するなら真
resolve: Boolean,
// ギャップ読み込み時の追加パラメータ
extra: String = "",
): Pair<TootApiResult?, TootResults?> { ): Pair<TootApiResult?, TootResults?> {
if (q.all { CharacterGroup.isWhitespace(it.code) }) {
return Pair(null, null)
}
val query = "q=${q.encodePercent()}&resolve=$resolve${
if (extra.isEmpty()) "" else "&$extra"
}"
var searchApiVersion = 2 var searchApiVersion = 2
var apiResult = request("/api/v2/search?$query") var apiResult = request("/api/v2/search?$query")
?: return Pair(null, null) ?: return Pair(null, null)
@ -1338,7 +1350,8 @@ suspend fun TootApiClient.syncAccountByUrl(
} else { } else {
val (apiResult, searchResult) = requestMastodonSearch( val (apiResult, searchResult) = requestMastodonSearch(
parser, parser,
"q=${whoUrl.encodePercent()}&resolve=true" q = whoUrl,
resolve = true,
) )
val ar = searchResult?.accounts?.firstOrNull() val ar = searchResult?.accounts?.firstOrNull()
if (apiResult != null && apiResult.error == null && ar == null) { if (apiResult != null && apiResult.error == null && ar == null) {
@ -1380,7 +1393,8 @@ suspend fun TootApiClient.syncAccountByAcct(
} else { } else {
val (apiResult, searchResult) = requestMastodonSearch( val (apiResult, searchResult) = requestMastodonSearch(
parser, parser,
"q=${acct.ascii.encodePercent()}&resolve=true" q = acct.ascii,
resolve = true,
) )
val ar = searchResult?.accounts?.firstOrNull() val ar = searchResult?.accounts?.firstOrNull()
if (apiResult != null && apiResult.error == null && ar == null) { if (apiResult != null && apiResult.error == null && ar == null) {
@ -1450,7 +1464,8 @@ suspend fun TootApiClient.syncStatus(
} else { } else {
val (apiResult, searchResult) = requestMastodonSearch( val (apiResult, searchResult) = requestMastodonSearch(
parser, parser,
"q=${url.encodePercent()}&resolve=true" q = url,
resolve = true,
) )
val targetStatus = searchResult?.statuses?.firstOrNull() val targetStatus = searchResult?.statuses?.firstOrNull()
if (apiResult != null && apiResult.error == null && targetStatus == null) { if (apiResult != null && apiResult.error == null && targetStatus == null) {

View File

@ -921,10 +921,13 @@ class ColumnTask_Gap(
column.listData.forEach { counter(it) } column.listData.forEach { counter(it) }
// https://mastodon2.juggler.jp/api/v2/search?q=gargron&type=accounts&offset=5 // https://mastodon2.juggler.jp/api/v2/search?q=gargron&type=accounts&offset=5
var query = "q=${column.searchQuery.encodePercent()}&type=$type&offset=$offset"
if (column.searchResolve) query += "&resolve=1"
val (apiResult, searchResult) = client.requestMastodonSearch(parser, query) val (apiResult, searchResult) = client.requestMastodonSearch(
parser,
q= column.searchQuery,
resolve = column.searchResolve,
extra = "type=$type&offset=$offset",
)
if (searchResult != null) { if (searchResult != null) {
listTmp = ArrayList() listTmp = ArrayList()
addAll(listTmp, searchResult.hashtags) addAll(listTmp, searchResult.hashtags)

View File

@ -1220,10 +1220,11 @@ class ColumnTask_Loading(
val (instance, instanceResult) = TootInstance.get(client) val (instance, instanceResult) = TootInstance.get(client)
instance ?: return instanceResult instance ?: return instanceResult
var query = "q=${column.searchQuery.encodePercent()}" val (apiResult, searchResult) = client.requestMastodonSearch(
if (column.searchResolve) query += "&resolve=1" parser,
q=column.searchQuery,
val (apiResult, searchResult) = client.requestMastodonSearch(parser, query) resolve = column.searchResolve,
)
if (searchResult != null) { if (searchResult != null) {
listTmp = ArrayList() listTmp = ArrayList()
addAll(listTmp, searchResult.hashtags) addAll(listTmp, searchResult.hashtags)

View File

@ -1,130 +1,130 @@
package jp.juggler.subwaytooter.notification //package jp.juggler.subwaytooter.notification
//
import android.app.Service //import android.app.Service
import android.content.Context //import android.content.Context
import android.content.Intent //import android.content.Intent
import android.os.IBinder //import android.os.IBinder
import android.os.SystemClock //import android.os.SystemClock
import androidx.core.content.ContextCompat //import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.global.appDispatchers //import jp.juggler.subwaytooter.global.appDispatchers
import jp.juggler.subwaytooter.notification.CheckerWakeLocks.Companion.checkerWakeLocks //import jp.juggler.subwaytooter.notification.CheckerWakeLocks.Companion.checkerWakeLocks
import jp.juggler.util.EmptyScope //import jp.juggler.util.EmptyScope
import jp.juggler.util.LogCategory //import jp.juggler.util.LogCategory
import jp.juggler.util.launchMain //import jp.juggler.util.launchMain
import kotlinx.coroutines.channels.Channel //import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException //import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.launch //import kotlinx.coroutines.launch
import kotlin.math.max //import kotlin.math.max
//
class ForegroundPollingService : Service() { //class ForegroundPollingService : Service() {
companion object { // companion object {
private val log = LogCategory("ForegroundPollingService") // private val log = LogCategory("ForegroundPollingService")
private const val NOTIFICATION_ID_FOREGROUND_POLLING = 4 // private const val NOTIFICATION_ID_FOREGROUND_POLLING = 4
private const val EXTRA_ACCOUNT_DB_ID = "accountDbId" // private const val EXTRA_ACCOUNT_DB_ID = "accountDbId"
private const val EXTRA_MESSAGE_ID = "messageId" // private const val EXTRA_MESSAGE_ID = "messageId"
//
fun start( // fun start(
context: Context, // context: Context,
messageId: String?, // messageId: String?,
dbId: Long, // dbId: Long,
) { // ) {
val intent = Intent(context, ForegroundPollingService::class.java).apply { // val intent = Intent(context, ForegroundPollingService::class.java).apply {
putExtra(EXTRA_ACCOUNT_DB_ID, dbId) // putExtra(EXTRA_ACCOUNT_DB_ID, dbId)
putExtra(EXTRA_MESSAGE_ID, messageId) // putExtra(EXTRA_MESSAGE_ID, messageId)
} // }
ContextCompat.startForegroundService(context, intent) // ContextCompat.startForegroundService(context, intent)
} // }
} // }
//
private class Item( // private class Item(
val accountDbId: Long, // val accountDbId: Long,
var lastRequired: Long = 0L, // var lastRequired: Long = 0L,
var lastHandled: Long = 0L, // var lastHandled: Long = 0L,
var lastStartId: Int = 0, // var lastStartId: Int = 0,
) // )
//
private val map = HashMap<Long, Item>() // private val map = HashMap<Long, Item>()
private val channel = Channel<Long>() // private val channel = Channel<Long>()
//
override fun onBind(intent: Intent?): IBinder? = null // override fun onBind(intent: Intent?): IBinder? = null
//
override fun onCreate() { // override fun onCreate() {
log.i("onCreate") // log.i("onCreate")
super.onCreate() // super.onCreate()
checkerWakeLocks(this).acquireWakeLocks() // checkerWakeLocks(this).acquireWakeLocks()
} // }
//
override fun onDestroy() { // override fun onDestroy() {
log.i("onDestroy") // log.i("onDestroy")
super.onDestroy() // super.onDestroy()
checkerWakeLocks(this).releasePowerLocks() // checkerWakeLocks(this).releasePowerLocks()
channel.close() // channel.close()
} // }
//
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val accountDbId = intent?.getLongExtra(EXTRA_ACCOUNT_DB_ID, -1L) ?: -1L // val accountDbId = intent?.getLongExtra(EXTRA_ACCOUNT_DB_ID, -1L) ?: -1L
val now = SystemClock.elapsedRealtime() // val now = SystemClock.elapsedRealtime()
log.i("onStartCommand startId=$startId, accountDbId=$accountDbId") // log.i("onStartCommand startId=$startId, accountDbId=$accountDbId")
synchronized(map) { // synchronized(map) {
map.getOrPut(accountDbId) { // map.getOrPut(accountDbId) {
Item(accountDbId = accountDbId) // Item(accountDbId = accountDbId)
}.apply { // }.apply {
lastRequired = now // lastRequired = now
lastStartId = startId // lastStartId = startId
} // }
} // }
launchMain { // launchMain {
try { // try {
channel.send(now) // channel.send(now)
} catch (ex: Throwable) { // } catch (ex: Throwable) {
log.trace(ex) // log.trace(ex)
} // }
} // }
return START_NOT_STICKY // return START_NOT_STICKY
} // }
//
init { // init {
EmptyScope.launch(appDispatchers.default) { // EmptyScope.launch(appDispatchers.default) {
var lastStartId = 0 // var lastStartId = 0
while (true) { // while (true) {
try { // try {
val target = synchronized(map) { // val target = synchronized(map) {
map.values // map.values
.filter { it.lastRequired > it.lastHandled } // .filter { it.lastRequired > it.lastHandled }
.minByOrNull { it.lastRequired } // .minByOrNull { it.lastRequired }
?.also { it.lastHandled = it.lastRequired } // ?.also { it.lastHandled = it.lastRequired }
} // }
if (target != null) { // if (target != null) {
lastStartId = max(lastStartId, target.lastStartId) // lastStartId = max(lastStartId, target.lastStartId)
check(target.accountDbId) // check(target.accountDbId)
continue // continue
} // }
log.i("stopSelf lastStartId=$lastStartId") // log.i("stopSelf lastStartId=$lastStartId")
if (lastStartId != 0) stopSelf(lastStartId) // if (lastStartId != 0) stopSelf(lastStartId)
channel.receive() // channel.receive()
} catch (ignored: ClosedReceiveChannelException) { // } catch (ignored: ClosedReceiveChannelException) {
log.i("channel closed.") // log.i("channel closed.")
break // break
} catch (ex: Throwable) { // } catch (ex: Throwable) {
log.trace(ex) // log.trace(ex)
} // }
} // }
} // }
} // }
//
private suspend fun check(accountDbId: Long) { // private suspend fun check(accountDbId: Long) {
try { // try {
PollingChecker( // PollingChecker(
context = this@ForegroundPollingService, // context = this@ForegroundPollingService,
accountDbId = accountDbId // accountDbId = accountDbId
).check { a, s -> // ).check { a, s ->
val text = "[${a.acct.pretty}]${s.desc}" // val text = "[${a.acct.pretty}]${s.desc}"
CheckerNotification.showMessage(this, text) { // CheckerNotification.showMessage(this, text) {
startForeground(NOTIFICATION_ID_FOREGROUND_POLLING, it) // startForeground(NOTIFICATION_ID_FOREGROUND_POLLING, it)
} // }
} // }
} catch (ex: Throwable) { // } catch (ex: Throwable) {
log.trace(ex) // log.trace(ex)
} // }
} // }
} //}

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.notification package jp.juggler.subwaytooter.notification
import android.app.ActivityManager
import android.content.Context import android.content.Context
import androidx.work.* import androidx.work.*
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
@ -91,10 +92,18 @@ class PollingWorker2(
} }
} }
private fun isAppForehround(): Boolean {
val processInfo = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(processInfo)
return processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
}
private suspend fun showMessage(text: String) = private suspend fun showMessage(text: String) =
CheckerNotification.showMessage(applicationContext, text) { CheckerNotification.showMessage(applicationContext, text) {
try { try {
setForeground(ForegroundInfo(NOTIFICATION_ID_POLLING_WORKER, it)) if(!isAppForehround()) error("app is not foreground.")
setForegroundAsync(ForegroundInfo(NOTIFICATION_ID_POLLING_WORKER,it))
.await()
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "showMessage failed.") log.e(ex, "showMessage failed.")
} }

View File

@ -7,11 +7,11 @@ buildscript {
ext.compile_sdk_version = 33 ext.compile_sdk_version = 33
ext.build_tools_version = "33.0.0" ext.build_tools_version = "33.0.0"
ext.appcompat_version = "1.5.0" ext.appcompat_version = "1.5.1"
ext.lifecycle_version = "2.5.1" ext.lifecycle_version = "2.5.1"
ext.arch_version = "2.1.0" ext.arch_version = "2.1.0"
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.7.20'
ext.kotlinx_coroutines_version = '1.6.4' ext.kotlinx_coroutines_version = '1.6.4'
ext.anko_version = '0.10.8' ext.anko_version = '0.10.8'
@ -30,13 +30,11 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.2.2' classpath 'com.android.tools.build:gradle:7.3.1'
// room google-services // room google-services
//noinspection GradleDependency classpath 'com.google.gms:google-services:4.3.14'
classpath 'com.google.gms:google-services:4.3.10'
//noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"