Coverage Summary for Class: ExportWorker (com.vsevolodganin.clicktrack.export)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| ExportWorker |
0%
(0/4)
|
0%
(0/22)
|
0%
(0/68)
|
0%
(0/410)
|
| ExportWorker$Companion |
0%
(0/1)
|
|
0%
(0/5)
|
0%
(0/22)
|
| ExportWorker$doWork$1 |
|
| ExportWorker$doWork$temporaryFile$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/20)
|
| ExportWorker$InputKeys |
|
| ExportWorker$MetroMembersInjector |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/72)
|
| ExportWorker$MetroMembersInjector$Companion |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/35)
|
| ExportWorker$NotificationGroups |
|
| Total |
0%
(0/8)
|
0%
(0/22)
|
0%
(0/76)
|
0%
(0/559)
|
package com.vsevolodganin.clicktrack.export
import android.Manifest
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.net.Uri
import android.os.Build
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.res.ResourcesCompat
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.vsevolodganin.clicktrack.IntentFactory
import com.vsevolodganin.clicktrack.applicationComponent
import com.vsevolodganin.clicktrack.common.MediaStoreAccess
import com.vsevolodganin.clicktrack.model.ClickTrack
import com.vsevolodganin.clicktrack.model.ClickTrackId
import com.vsevolodganin.clicktrack.model.ClickTrackWithDatabaseId
import com.vsevolodganin.clicktrack.multiplatform.R
import com.vsevolodganin.clicktrack.notification.NotificationChannels
import com.vsevolodganin.clicktrack.storage.ClickTrackRepository
import dev.zacsweers.metro.Inject
import kotlinx.coroutines.flow.firstOrNull
import kotlin.math.roundToInt
class ExportWorker(private val appContext: Context, workerParams: WorkerParameters) : CoroutineWorker(appContext, workerParams) {
companion object {
fun createWorkRequest(clickTrackId: ClickTrackId.Database): OneTimeWorkRequest {
return OneTimeWorkRequestBuilder<ExportWorker>()
.setInputData(
workDataOf(
InputKeys.CLICK_TRACK_ID to clickTrackId.value,
),
)
.build()
}
}
// Need unique id to show multiple progress notifications and we can't use string tags
private val foregroundNotificationId = id.leastSignificantBits.toInt()
@Inject
private lateinit var workManager: WorkManager
@Inject
private lateinit var clickTrackRepository: ClickTrackRepository
@Inject
private lateinit var exportToAudioFile: ExportToAudioFile
@Inject
private lateinit var mediaStoreAccess: MediaStoreAccess
@Inject
private lateinit var notificationManager: NotificationManagerCompat
@Inject
private lateinit var notificationChannels: NotificationChannels
@Inject
private lateinit var intentFactory: IntentFactory
init {
appContext.applicationComponent
.exportWorkerComponentFactory
.create()
.inject(this)
}
override suspend fun doWork(): Result {
val clickTrackId = inputData.getLong(InputKeys.CLICK_TRACK_ID, -1)
.takeIf { it >= 0 }
?.let(ClickTrackId::Database)
?: return Result.failure()
val clickTrack = clickTrackRepository.getById(clickTrackId).firstOrNull()
?: return Result.failure()
setForeground(foregroundInfo(clickTrack, 0f))
val temporaryFile = exportToAudioFile.export(
clickTrack = clickTrack.value,
reportProgress = {
setForeground(foregroundInfo(clickTrack, it))
},
) ?: return Result.failure()
val accessUri = mediaStoreAccess.addAudioFile(temporaryFile)
?: return Result.failure()
// Just speeding things up, this file will be deleted by system anyway
temporaryFile.delete()
notificationManager.cancel(foregroundNotificationId)
notifyFinished(clickTrack.value, accessUri)
return Result.success()
}
private fun foregroundInfo(clickTrack: ClickTrackWithDatabaseId, progress: Float): ForegroundInfo {
val tapIntent = intentFactory.navigateClickTrack(clickTrack.id)
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val progressResolution = 100
return ForegroundInfo(
foregroundNotificationId,
NotificationCompat.Builder(appContext, notificationChannels.export)
.setContentTitle(appContext.getString(R.string.export_worker_notification_in_process, clickTrack.value.name))
.setSmallIcon(R.drawable.ic_notification)
.setColor(ResourcesCompat.getColor(appContext.resources, R.color.blood_red, null))
.setOngoing(true)
.setContentIntent(PendingIntent.getActivity(appContext, 0, tapIntent, pendingIntentFlags))
.addAction(
android.R.drawable.ic_delete,
appContext.getString(android.R.string.cancel),
workManager.createCancelPendingIntent(id),
)
.setGroup(NotificationGroups.EXPORTING)
.setProgress(progressResolution, (progress * progressResolution).roundToInt(), false)
.build(),
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
}
private fun notifyFinished(clickTrack: ClickTrack, accessUri: Uri) {
val tapIntent = Intent.createChooser(
Intent(Intent.ACTION_VIEW).apply {
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK
setDataAndType(accessUri, "audio/*")
},
null,
)
val pendingIntentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
if (ActivityCompat.checkSelfPermission(appContext, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {
notificationManager.notify(
clickTrack.name,
R.id.notification_export_finished,
NotificationCompat.Builder(appContext, notificationChannels.export)
.setContentTitle(appContext.getString(R.string.export_worker_notification_finished, clickTrack.name))
.setContentText(appContext.getString(R.string.export_worker_notification_open))
.setSmallIcon(R.drawable.ic_notification)
.setColor(ResourcesCompat.getColor(appContext.resources, R.color.blood_red, null))
.setContentIntent(PendingIntent.getActivity(appContext, 0, tapIntent, pendingIntentFlags))
.setGroup(NotificationGroups.EXPORT_FINISHED)
.build(),
)
}
}
private object InputKeys {
const val CLICK_TRACK_ID = "click_track_id"
}
private object NotificationGroups {
const val EXPORTING = "com.vsevolodganin.clicktrack.exporting"
const val EXPORT_FINISHED = "com.vsevolodganin.clicktrack.export_finished"
}
}