- 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 {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
defaultConfig {
@ -128,6 +129,10 @@ dependencies {
implementation fileTree(include: ['*.jar'], dir: '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
// https://github.com/android/android-test/issues/1022
androidTestImplementation "androidx.test:core:1.4.0"
@ -158,9 +163,9 @@ dependencies {
implementation "androidx.drawerlayout:drawerlayout:1.1.1"
// 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
implementation "androidx.browser:browser:1.4.0"
@ -168,10 +173,10 @@ dependencies {
// Recyclerview
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
implementation "com.google.firebase:firebase-messaging:23.0.8"
implementation "com.google.firebase:firebase-messaging:23.1.0"
implementation "org.jetbrains.kotlin:kotlin-reflect"
testImplementation "org.jetbrains.kotlin:kotlin-test"
@ -262,7 +267,7 @@ dependencies {
// optional - Test helpers for LiveData
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"
implementation "androidx.room:room-runtime:$roomVersion"

View File

@ -354,9 +354,6 @@
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<service
android:name="jp.juggler.subwaytooter.notification.ForegroundPollingService"
android:exported="false" />
<provider
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.RemoteMessage
import jp.juggler.subwaytooter.notification.ForegroundPollingService
import jp.juggler.subwaytooter.notification.PollingChecker
import jp.juggler.subwaytooter.notification.restartAllWorker
import jp.juggler.subwaytooter.pref.PrefDevice
import jp.juggler.subwaytooter.table.NotificationCache
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.LogCategory
import kotlinx.coroutines.runBlocking
import java.util.*
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) {
val context = this
val messageId = remoteMessage.messageId ?: return
if (isDuplicateMessage(messageId)) return
val accounts = ArrayList<SavedAccount>()
for ((key, value) in remoteMessage.data) {
log.w("onMessageReceived: $key=$value")
@ -60,19 +74,26 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
NotificationCache.resetLastLoad()
accounts.addAll(SavedAccount.loadAccountList(context))
}
log.i("accounts.size=${accounts.size}")
accounts.forEach {
ForegroundPollingService.start(this, remoteMessage.messageId, it.db_id)
log.i("accounts.size=${accounts.size} thred=${Thread.currentThread().name}")
runBlocking {
accounts.forEach {
check(it.db_id)
}
}
}
override fun onNewToken(token: String) {
private suspend fun check(accountDbId: Long) {
try {
log.w("onTokenRefresh: token=$token")
PrefDevice.from(this).edit().putString(PrefDevice.KEY_DEVICE_TOKEN, token).apply()
restartAllWorker(this)
PollingChecker(
context = this,
accountDbId = accountDbId
).check { a, s ->
val text = "[${a.acct.pretty}]${s.desc}"
log.i(text)
}
} 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
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 =
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(
parser: TootParser,
query: String,
// 検索文字列
q: String,
// リモートサーバの情報を解決するなら真
resolve: Boolean,
// ギャップ読み込み時の追加パラメータ
extra: String = "",
): 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 apiResult = request("/api/v2/search?$query")
?: return Pair(null, null)
@ -1338,7 +1350,8 @@ suspend fun TootApiClient.syncAccountByUrl(
} else {
val (apiResult, searchResult) = requestMastodonSearch(
parser,
"q=${whoUrl.encodePercent()}&resolve=true"
q = whoUrl,
resolve = true,
)
val ar = searchResult?.accounts?.firstOrNull()
if (apiResult != null && apiResult.error == null && ar == null) {
@ -1380,7 +1393,8 @@ suspend fun TootApiClient.syncAccountByAcct(
} else {
val (apiResult, searchResult) = requestMastodonSearch(
parser,
"q=${acct.ascii.encodePercent()}&resolve=true"
q = acct.ascii,
resolve = true,
)
val ar = searchResult?.accounts?.firstOrNull()
if (apiResult != null && apiResult.error == null && ar == null) {
@ -1450,7 +1464,8 @@ suspend fun TootApiClient.syncStatus(
} else {
val (apiResult, searchResult) = requestMastodonSearch(
parser,
"q=${url.encodePercent()}&resolve=true"
q = url,
resolve = true,
)
val targetStatus = searchResult?.statuses?.firstOrNull()
if (apiResult != null && apiResult.error == null && targetStatus == null) {

View File

@ -921,10 +921,13 @@ class ColumnTask_Gap(
column.listData.forEach { counter(it) }
// 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) {
listTmp = ArrayList()
addAll(listTmp, searchResult.hashtags)

View File

@ -1220,10 +1220,11 @@ class ColumnTask_Loading(
val (instance, instanceResult) = TootInstance.get(client)
instance ?: return instanceResult
var query = "q=${column.searchQuery.encodePercent()}"
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,
)
if (searchResult != null) {
listTmp = ArrayList()
addAll(listTmp, searchResult.hashtags)

View File

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

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.notification
import android.app.ActivityManager
import android.content.Context
import androidx.work.*
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) =
CheckerNotification.showMessage(applicationContext, text) {
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) {
log.e(ex, "showMessage failed.")
}

View File

@ -7,11 +7,11 @@ buildscript {
ext.compile_sdk_version = 33
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.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.anko_version = '0.10.8'
@ -30,13 +30,11 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.android.tools.build:gradle:7.3.1'
// room google-services
//noinspection GradleDependency
classpath 'com.google.gms:google-services:4.3.10'
classpath 'com.google.gms:google-services:4.3.14'
//noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"