SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/notification/PollingWorker2.kt

207 lines
7.2 KiB
Kotlin

package jp.juggler.subwaytooter.notification
import android.app.ActivityManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.pref.prefDevice
import jp.juggler.util.log.LogCategory
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.coroutineScope
import java.util.concurrent.TimeUnit
import kotlin.math.max
/*
- WorkManagerのWorker。
- アカウント別にuniqueWorkNameを持つ。
- アプリが背面にいる間は進捗表示を通知で行う。
*/
class PollingWorker2(
context: Context,
workerParameters: WorkerParameters,
) : CoroutineWorker(context, workerParameters) {
companion object {
private val log = LogCategory("PollingWorker")
private const val KEY_ACCOUNT_DB_ID = "accountDbId"
private const val WORK_NAME = "PollingWorker2"
private val nc = NotificationChannels.PullWorker
private var lastMessage: String? = null
suspend fun cancelPolling(context: Context) {
val isOk = WorkManager.getInstance(context)
.cancelUniqueWork(WORK_NAME).await()
log.i("cancelPolling isOk=$isOk")
}
suspend fun enqueuePolling(
context: Context,
) {
val workManager = WorkManager.getInstance(context)
val prefDevice = context.prefDevice
val newInterval = max(
PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS,
(PrefS.spPullNotificationCheckInterval.value.toLongOrNull() ?: 0L) * 60000L,
)
// アカウント毎に呼び出されるので重複排除したい
// 未完了のジョブがあり、インターバルが同じなら何もしない
if (workManager.getWorkInfosForUniqueWork(WORK_NAME).await()
.any {
val oldInterval = prefDevice.pollingWorker2Interval ?: 0L
oldInterval == newInterval && !it.state.isFinished
}
) {
return
}
// 登録し直す
val data = Data.Builder().build()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val workRequest = PeriodicWorkRequestBuilder<PollingWorker2>(
newInterval,
TimeUnit.MILLISECONDS,
// flexTimeInterval
// 決まった周期の間の末尾からflexTimeIntervalを引いた時刻の間の何処かで処理が実行される
// (周期より短い範囲で)大きい値の方が「より早いタイミングで」実行されてテストに良い
// (また、setInitialDelayはその何処かの範囲にないと効果がない)
newInterval - 1000L,
TimeUnit.MILLISECONDS,
)
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
.setConstraints(constraints)
.setInputData(data)
.build()
workManager.enqueueUniquePeriodicWork(
WORK_NAME,
ExistingPeriodicWorkPolicy.UPDATE,
workRequest
).await()
prefDevice.pollingWorker2Interval = newInterval
}
}
private fun isAppForehround(): Boolean {
val processInfo = ActivityManager.RunningAppProcessInfo()
ActivityManager.getMyMemoryState(processInfo)
return processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
}
private suspend fun showMessage(text: String) {
try {
messageToForegroundInfo(text)
?.let { setForegroundAsync(it).await() }
} catch (ex: Throwable) {
log.e(ex, "showMessage failed.")
}
}
private fun stateMapToString(map: Map<PollingState, List<String>>) =
StringBuilder().apply {
for (state in PollingState.entries) {
val list = map[state] ?: continue
if (isNotEmpty()) append(" |")
append(state.desc)
append(": ")
if (list.size <= 2) {
append(list.sorted().joinToString(", "))
} else {
append("${list.size}")
}
}
}.toString()
private suspend fun workImpl() {
val context = applicationContext
coroutineScope {
if (importProtector.get()) {
log.w("abort by importProtector.")
return@coroutineScope
}
App1.prepare(context, "doWork")
showMessage(context.getString(R.string.loading_notification_title))
checkNoticifationAll(context, "") { map ->
showMessage(stateMapToString(map))
}
}
}
override suspend fun doWork(): Result {
try {
workImpl()
} catch (ex: Throwable) {
when (ex) {
is CancellationException -> log.e("doWork cancelled.")
else -> log.e(ex, "doWork failed.")
}
}
return Result.success()
}
private fun messageToForegroundInfo(
text: String,
force: Boolean = false,
): ForegroundInfo? {
// テキストが変化していないなら更新しない
if (!force && (text.isEmpty() || text == lastMessage)) return null
lastMessage = text
log.i(text)
val context = applicationContext
// 通知タップ時のPendingIntent
val iTap = Intent(context, ActMain::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val piTap = PendingIntent.getActivity(
context,
nc.pircTap,
iTap,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return nc.createForegroundInfo(
context,
text = text,
piTap = piTap,
force = force,
)
}
/**
* ワーカーの初期化時にOSから呼ばれる場合がある
* - Android 11 moto g31 で発生
* - ダミーメッセージを仕込んだForegroundInfoを返す
*/
override suspend fun getForegroundInfo(): ForegroundInfo =
messageToForegroundInfo("initializing…", force = true)!!
}