Добавлен foreground сервис
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
@@ -24,6 +28,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".tasks.TaskPipelineForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,9 +1,14 @@
|
||||
package com.github.nullptroma.wallenc.app
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.nullptroma.wallenc.presentation.WallencUi
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import timber.log.Timber
|
||||
@@ -15,6 +20,7 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
requestNotificationPermissionIfNeeded()
|
||||
|
||||
Timber.plant(Timber.DebugTree())
|
||||
// val sdk = YandexAuthSdk.create(YandexAuthOptions(applicationContext, true))
|
||||
@@ -27,6 +33,24 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestNotificationPermissionIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
|
||||
val granted = ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) return
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
NOTIFICATION_PERMISSION_REQUEST_CODE,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 100
|
||||
}
|
||||
|
||||
// private fun handleResult(result: YandexAuthResult) {
|
||||
// when (result) {
|
||||
// is YandexAuthResult.Success -> Toast.makeText(applicationContext, "Success: ${result.token}", Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
package com.github.nullptroma.wallenc.app
|
||||
|
||||
import android.app.Application
|
||||
import com.github.nullptroma.wallenc.app.tasks.TaskPipelineForegroundBootstrap
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class WallencApplication : Application()
|
||||
class WallencApplication : Application() {
|
||||
|
||||
@Inject
|
||||
lateinit var taskPipelineForegroundBootstrap: TaskPipelineForegroundBootstrap
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
taskPipelineForegroundBootstrap.start()
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,11 @@ import com.github.nullptroma.wallenc.data.db.app.dao.StorageMetaInfoDao
|
||||
import com.github.nullptroma.wallenc.data.db.app.repository.StorageKeyMapRepository
|
||||
import com.github.nullptroma.wallenc.data.db.app.repository.StorageMetaInfoRepository
|
||||
import com.github.nullptroma.wallenc.data.storages.UnlockManager
|
||||
import com.github.nullptroma.wallenc.data.tasks.TaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.data.vaults.VaultsManager
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IUnlockManager
|
||||
import com.github.nullptroma.wallenc.domain.interfaces.IVaultsManager
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -21,6 +23,12 @@ import javax.inject.Singleton
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class SingletonModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTaskOrchestrator(
|
||||
@IoDispatcher ioDispatcher: CoroutineDispatcher,
|
||||
): ITaskOrchestrator = TaskOrchestrator(ioDispatcher)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVaultsManager(
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.github.nullptroma.wallenc.app.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class TaskPipelineForegroundBootstrap @Inject constructor(
|
||||
@ApplicationContext private val app: Context,
|
||||
private val orchestrator: ITaskOrchestrator,
|
||||
) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
fun start() {
|
||||
scope.launch {
|
||||
orchestrator.foregroundUi.collect { ui ->
|
||||
if (ui is TaskForegroundUiState.Visible) {
|
||||
ContextCompat.startForegroundService(
|
||||
app,
|
||||
Intent(app, TaskPipelineForegroundService::class.java),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.github.nullptroma.wallenc.app.tasks
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.github.nullptroma.wallenc.app.R
|
||||
import com.github.nullptroma.wallenc.domain.tasks.ITaskOrchestrator
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskForegroundUiState
|
||||
import com.github.nullptroma.wallenc.domain.tasks.TaskProgress
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@AndroidEntryPoint
|
||||
class TaskPipelineForegroundService : Service() {
|
||||
|
||||
@Inject
|
||||
lateinit var orchestrator: ITaskOrchestrator
|
||||
|
||||
private val serviceJob = SupervisorJob()
|
||||
private val serviceScope = CoroutineScope(serviceJob + Dispatchers.Main.immediate)
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ensureChannel()
|
||||
startForeground(NOTIFICATION_ID, buildPlaceholderNotification())
|
||||
|
||||
serviceScope.launch {
|
||||
var sawVisible = false
|
||||
var pendingUi: TaskForegroundUiState.Visible? = null
|
||||
var lastNotificationAtMs = 0L
|
||||
var delayedFlushJob: Job? = null
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
|
||||
fun pushVisible(ui: TaskForegroundUiState.Visible) {
|
||||
val notification = buildProgressNotification(ui.title, ui.progress)
|
||||
nm.notify(NOTIFICATION_ID, notification)
|
||||
lastNotificationAtMs = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
orchestrator.foregroundUi.collect { ui ->
|
||||
when (ui) {
|
||||
is TaskForegroundUiState.Visible -> {
|
||||
sawVisible = true
|
||||
pendingUi = ui
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val elapsed = now - lastNotificationAtMs
|
||||
if (elapsed >= MIN_NOTIFICATION_UPDATE_INTERVAL_MS) {
|
||||
delayedFlushJob?.cancel()
|
||||
delayedFlushJob = null
|
||||
pushVisible(ui)
|
||||
pendingUi = null
|
||||
} else if (delayedFlushJob == null) {
|
||||
delayedFlushJob = serviceScope.launch {
|
||||
delay(MIN_NOTIFICATION_UPDATE_INTERVAL_MS - elapsed)
|
||||
pendingUi?.let { last ->
|
||||
pushVisible(last)
|
||||
pendingUi = null
|
||||
}
|
||||
delayedFlushJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TaskForegroundUiState.Hidden -> {
|
||||
if (sawVisible) {
|
||||
delayedFlushJob?.cancel()
|
||||
delayedFlushJob = null
|
||||
pendingUi?.let { last ->
|
||||
// Flush latest state before removing notification.
|
||||
pushVisible(last)
|
||||
pendingUi = null
|
||||
}
|
||||
sawVisible = false
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (intent?.action == ACTION_CANCEL_ALL) {
|
||||
orchestrator.cancelAll()
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
serviceScope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun ensureChannel() {
|
||||
val nm = getSystemService(NotificationManager::class.java)
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
getString(R.string.task_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_LOW,
|
||||
)
|
||||
nm.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun buildPlaceholderNotification(): Notification =
|
||||
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.task_notification_title))
|
||||
.setContentText(getString(R.string.task_notification_preparing))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.build()
|
||||
|
||||
private fun buildProgressNotification(title: String, progress: TaskProgress?): Notification {
|
||||
val cancelIntent = Intent(this, TaskPipelineForegroundService::class.java).apply {
|
||||
action = ACTION_CANCEL_ALL
|
||||
}
|
||||
val cancelPending = PendingIntent.getService(
|
||||
this,
|
||||
1,
|
||||
cancelIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.addAction(
|
||||
android.R.drawable.ic_menu_close_clear_cancel,
|
||||
getString(R.string.task_notification_cancel),
|
||||
cancelPending,
|
||||
)
|
||||
|
||||
val label = progress?.label
|
||||
val fraction = progress?.fraction
|
||||
if (fraction != null) {
|
||||
val pct = (fraction.coerceIn(0f, 1f) * 100).roundToInt()
|
||||
builder.setContentText(label ?: "$pct%")
|
||||
builder.setProgress(100, pct, false)
|
||||
} else {
|
||||
builder.setContentText(label ?: getString(R.string.task_notification_indeterminate))
|
||||
builder.setProgress(0, 0, true)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CHANNEL_ID = "wallenc_task_pipeline"
|
||||
private const val NOTIFICATION_ID = 1001
|
||||
private const val MIN_NOTIFICATION_UPDATE_INTERVAL_MS = 500L
|
||||
const val ACTION_CANCEL_ALL = "com.github.nullptroma.wallenc.CANCEL_ALL_TASKS"
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
<resources>
|
||||
<string name="app_name">Wallenc</string>
|
||||
<string name="task_notification_channel_name">Background tasks</string>
|
||||
<string name="task_notification_title">Wallenc tasks</string>
|
||||
<string name="task_notification_preparing">Preparing…</string>
|
||||
<string name="task_notification_indeterminate">Working…</string>
|
||||
<string name="task_notification_cancel">Cancel</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user