DownloadWorker: simplify & improve code [1/2]
TODO: - Reduce notification frequency, create too many notifications resulting in notification actions being inaccesible. - Its a bad practive to bombard system with notification redraws
This commit is contained in:
@@ -160,6 +160,11 @@
|
|||||||
android:name=".data.receiver.InstallerStatusReceiver"
|
android:name=".data.receiver.InstallerStatusReceiver"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- Cancel Download Receiver-->
|
||||||
|
<receiver
|
||||||
|
android:name=".data.receiver.DownloadCancelReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- SessionInstaller (as device owner) -->
|
<!-- SessionInstaller (as device owner) -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.receiver.DeviceOwnerReceiver"
|
android:name=".data.receiver.DeviceOwnerReceiver"
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Aurora OSS
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.aurora.store.data.receiver
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import com.aurora.store.AuroraApp
|
||||||
|
import com.aurora.store.data.helper.DownloadHelper
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class DownloadCancelReceiver : BroadcastReceiver() {
|
||||||
|
private val TAG = DownloadCancelReceiver::class.java.simpleName
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var downloadHelper: DownloadHelper
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
val packageName: String = intent?.getStringExtra("PACKAGE_NAME") ?: ""
|
||||||
|
|
||||||
|
if (packageName.isNotBlank()) {
|
||||||
|
Log.d(TAG, "Received cancel download request for $packageName")
|
||||||
|
AuroraApp.scope.launch(Dispatchers.IO) {
|
||||||
|
downloadHelper.cancelDownload(packageName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Aurora OSS
|
||||||
|
* SPDX-FileCopyrightText: 2025 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
package com.aurora.store.data.work
|
package com.aurora.store.data.work
|
||||||
|
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
@@ -38,7 +44,6 @@ import dagger.assisted.Assisted
|
|||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
@@ -70,27 +75,32 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
private var notificationId: Int = 200
|
private var notificationId: Int = 200
|
||||||
private var icon: Bitmap? = null
|
private var icon: Bitmap? = null
|
||||||
private var downloading = false
|
|
||||||
private var totalBytes by Delegates.notNull<Long>()
|
private var totalBytes by Delegates.notNull<Long>()
|
||||||
private var totalProgress = 0
|
private var totalProgress = 0
|
||||||
private var downloadedBytes = 0L
|
private var downloadedBytes = 0L
|
||||||
|
|
||||||
private val TAG = DownloadWorker::class.java.simpleName
|
private val TAG = DownloadWorker::class.java.simpleName
|
||||||
|
|
||||||
|
object Exceptions {
|
||||||
|
val InvalidAuthDataException = Exception("AuthData is invalid")
|
||||||
|
val NoPackageNameException = Exception("No packagename provided")
|
||||||
|
val NothingToDownloadException = Exception("Failed to purchase app")
|
||||||
|
val DownloadFailedException = Exception("Download was failed or cancelled")
|
||||||
|
val VerificationFailedException = Exception("Verification of downloaded files failed")
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
super.doWork()
|
super.doWork()
|
||||||
|
|
||||||
// Bail out immediately if authData is not valid
|
// Bail out immediately if authData is not valid
|
||||||
if (!authProvider.isSavedAuthDataValid()) {
|
if (!authProvider.isSavedAuthDataValid()) return onFailure(Exceptions.InvalidAuthDataException)
|
||||||
Log.e(TAG, "AuthData is not valid, exiting!")
|
|
||||||
onFailure()
|
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch required data for download
|
// Fetch required data for download
|
||||||
try {
|
try {
|
||||||
val packageName =
|
val packageName = inputData.getString(DownloadHelper.PACKAGE_NAME)
|
||||||
inputData.getString(DownloadHelper.PACKAGE_NAME) ?: return Result.failure()
|
|
||||||
|
// Bail out if no package name is provided
|
||||||
|
if (packageName.isNullOrBlank()) return onFailure(Exceptions.NoPackageNameException)
|
||||||
|
|
||||||
notificationId = packageName.hashCode()
|
notificationId = packageName.hashCode()
|
||||||
download = downloadDao.getDownload(packageName)
|
download = downloadDao.getDownload(packageName)
|
||||||
@@ -102,110 +112,131 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
icon = Bitmap.createScaledBitmap(bitmap, 96, 96, true)
|
icon = Bitmap.createScaledBitmap(bitmap, 96, 96, true)
|
||||||
}
|
}
|
||||||
} catch (exception: Exception) {
|
} catch (exception: Exception) {
|
||||||
Log.e(TAG, "Failed to parse download data", exception)
|
return onFailure(exception)
|
||||||
onFailure(exception)
|
|
||||||
return Result.failure()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set work/service to foreground on < Android 12.0
|
// Set work/service to foreground on < Android 12.0
|
||||||
setForeground(getForegroundInfo())
|
setForeground(getForegroundInfo())
|
||||||
|
|
||||||
// Bail out if file list is empty
|
// Try to purchase the app if file list is empty
|
||||||
download.fileList = download.fileList.ifEmpty {
|
download.fileList = download.fileList.ifEmpty {
|
||||||
purchase(download.packageName, download.versionCode, download.offerType)
|
purchase(download.packageName, download.versionCode, download.offerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (download.fileList.isEmpty()) {
|
// Bail out if file list is empty after purchase
|
||||||
Log.i(TAG, "Nothing to download!")
|
if (download.fileList.isEmpty()) return onFailure(Exceptions.NothingToDownloadException)
|
||||||
onFailure()
|
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create dirs & generate download request for files and shared libs (if any)
|
// Create dirs & generate download request for files and shared libs (if any)
|
||||||
PathUtil.getAppDownloadDir(appContext, download.packageName, download.versionCode).mkdirs()
|
PathUtil.getAppDownloadDir(appContext, download.packageName, download.versionCode).mkdirs()
|
||||||
|
|
||||||
|
// Create OBB dir if required
|
||||||
if (download.fileList.requiresObbDir()) {
|
if (download.fileList.requiresObbDir()) {
|
||||||
PathUtil.getObbDownloadDir(download.packageName).mkdirs()
|
PathUtil.getObbDownloadDir(download.packageName).mkdirs()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purchase the app (free apps needs to be purchased too)
|
val files = mutableListOf<GPlayFile>()
|
||||||
val requestList = mutableListOf<GPlayFile>()
|
|
||||||
|
// Check if shared libs are present, if yes, handle them first
|
||||||
if (download.sharedLibs.isNotEmpty()) {
|
if (download.sharedLibs.isNotEmpty()) {
|
||||||
download.sharedLibs.forEach {
|
download.sharedLibs.forEach {
|
||||||
|
// Create shared lib download dir
|
||||||
PathUtil.getLibDownloadDir(
|
PathUtil.getLibDownloadDir(
|
||||||
appContext,
|
appContext,
|
||||||
download.packageName,
|
download.packageName,
|
||||||
download.versionCode,
|
download.versionCode,
|
||||||
it.packageName
|
it.packageName
|
||||||
).mkdirs()
|
).mkdirs()
|
||||||
it.fileList = it.fileList.ifEmpty { purchase(it.packageName, it.versionCode, 0) }
|
|
||||||
requestList.addAll(it.fileList)
|
// Purchase shared lib if file list is empty
|
||||||
|
it.fileList = it.fileList.ifEmpty {
|
||||||
|
purchase(it.packageName, it.versionCode, 0)
|
||||||
|
}
|
||||||
|
files.addAll(it.fileList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
requestList.addAll(download.fileList)
|
files.addAll(download.fileList)
|
||||||
|
|
||||||
// Update data for notification
|
// Update data for notification
|
||||||
download.totalFiles = requestList.size
|
download.totalFiles = files.size
|
||||||
totalBytes = requestList.sumOf { it.size }
|
totalBytes = files.sumOf { it.size }
|
||||||
|
|
||||||
// Update database with all latest purchases
|
// Update database with all latest purchases
|
||||||
downloadDao.updateFiles(download.packageName, download.fileList)
|
downloadDao.updateFiles(download.packageName, download.fileList)
|
||||||
downloadDao.updateSharedLibs(download.packageName, download.sharedLibs)
|
downloadDao.updateSharedLibs(download.packageName, download.sharedLibs)
|
||||||
|
|
||||||
// Download and verify all files exists
|
// Download files
|
||||||
requestList.forEach { request ->
|
val downloadSuccess = downloadFiles(download.packageName, files)
|
||||||
downloading = true
|
|
||||||
runCatching { downloadFile(request); download.downloadedFiles++ }
|
|
||||||
.onSuccess { downloading = false }
|
|
||||||
.onFailure {
|
|
||||||
Log.e(TAG, "Failed to download ${download.packageName}", it)
|
|
||||||
downloading = false
|
|
||||||
|
|
||||||
onFailure(it as Exception)
|
// Report failure if download was stopped or failed
|
||||||
|
if (!downloadSuccess || isStopped) return onFailure(Exceptions.DownloadFailedException)
|
||||||
|
|
||||||
return Result.failure()
|
// Verify downloaded files
|
||||||
}
|
val verifySuccess = verifyFiles(download.packageName, files)
|
||||||
while (downloading) {
|
|
||||||
delay(1000)
|
|
||||||
val d = downloadDao.getDownload(download.packageName)
|
|
||||||
if (isStopped || d.downloadStatus == DownloadStatus.CANCELLED) {
|
|
||||||
onFailure(CancellationException())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
// Report failure if verification failed
|
||||||
// Verify all downloaded files
|
if (!verifySuccess || isStopped) return onFailure(Exceptions.VerificationFailedException)
|
||||||
Log.i(TAG, "Verifying downloaded files")
|
|
||||||
notifyStatus(DownloadStatus.VERIFYING)
|
|
||||||
requestList.forEach { require(verifyFile(it)) }
|
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to verify downloaded files!", exception)
|
|
||||||
onFailure(exception)
|
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark download as completed
|
Log.i(TAG, "Finished downloading & verifying ${download.packageName}")
|
||||||
onSuccess()
|
notifyStatus(DownloadStatus.COMPLETED)
|
||||||
return Result.success()
|
|
||||||
|
return onSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onSuccess() {
|
private suspend fun downloadFiles(
|
||||||
withContext(NonCancellable) {
|
packageName: String,
|
||||||
Log.i(TAG, "Finished downloading ${download.packageName}")
|
files: List<GPlayFile>
|
||||||
notifyStatus(DownloadStatus.COMPLETED)
|
): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
for (file in files) {
|
||||||
try {
|
try {
|
||||||
appInstaller.getPreferredInstaller().install(download)
|
downloadFile(file)
|
||||||
} catch (exception: Exception) {
|
|
||||||
Log.e(TAG, "Failed to install ${download.packageName}", exception)
|
if (isStopped) {
|
||||||
|
Log.w(TAG, "Download cancelled for $packageName")
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
|
||||||
|
download.downloadedFiles++
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to download $packageName", e)
|
||||||
|
return@withContext false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun verifyFiles(
|
||||||
|
packageName: String,
|
||||||
|
files: List<GPlayFile>
|
||||||
|
): Boolean = withContext(NonCancellable) {
|
||||||
|
notifyStatus(DownloadStatus.VERIFYING)
|
||||||
|
|
||||||
|
for (file in files) {
|
||||||
|
try {
|
||||||
|
verifyFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to verify $packageName : ${file.name}", e)
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun onSuccess(): Result = withContext(NonCancellable) {
|
||||||
|
return@withContext try {
|
||||||
|
appInstaller.getPreferredInstaller().install(download)
|
||||||
|
Result.success()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Failed to install ${download.packageName}", exception)
|
||||||
|
onFailure(exception)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onFailure(exception: Exception = Exception("Something went wrong!")) {
|
private suspend fun onFailure(exception: Exception = Exception("Something went wrong!")): Result =
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
Log.i(TAG, "Failed downloading ${download.packageName}")
|
Log.e(TAG, "Job failed: ${download.packageName}", exception)
|
||||||
|
|
||||||
val cancelReasons = listOf(STOP_REASON_USER, STOP_REASON_CANCELLED_BY_APP)
|
val cancelReasons = listOf(STOP_REASON_USER, STOP_REASON_CANCELLED_BY_APP)
|
||||||
if (isSAndAbove && stopReason in cancelReasons) {
|
if (isSAndAbove && stopReason in cancelReasons) {
|
||||||
notifyStatus(DownloadStatus.CANCELLED)
|
notifyStatus(DownloadStatus.CANCELLED)
|
||||||
@@ -221,9 +252,11 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove all notifications
|
||||||
notificationManager.cancel(notificationId)
|
notificationManager.cancel(notificationId)
|
||||||
|
|
||||||
|
return@withContext Result.failure()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Purchases the app to get the download URL of the required files
|
* Purchases the app to get the download URL of the required files
|
||||||
@@ -258,6 +291,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
* @return A [Result] indicating whether the file was downloaded or not.
|
* @return A [Result] indicating whether the file was downloaded or not.
|
||||||
*/
|
*/
|
||||||
private suspend fun downloadFile(gFile: GPlayFile): Result {
|
private suspend fun downloadFile(gFile: GPlayFile): Result {
|
||||||
|
Log.i(TAG, "Downloading ${gFile.name}")
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val file = PathUtil.getLocalFile(appContext, gFile, download)
|
val file = PathUtil.getLocalFile(appContext, gFile, download)
|
||||||
|
|
||||||
@@ -338,7 +372,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
override suspend fun getForegroundInfo(): ForegroundInfo {
|
override suspend fun getForegroundInfo(): ForegroundInfo {
|
||||||
val notification = if (this::download.isInitialized) {
|
val notification = if (this::download.isInitialized) {
|
||||||
NotificationUtil.getDownloadNotification(appContext, download, id, icon)
|
NotificationUtil.getDownloadNotification(appContext, download, icon)
|
||||||
} else {
|
} else {
|
||||||
NotificationUtil.getDownloadNotification(appContext)
|
NotificationUtil.getDownloadNotification(appContext)
|
||||||
}
|
}
|
||||||
@@ -353,7 +387,6 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
/**
|
/**
|
||||||
* Notifies the user of the current status of the download.
|
* Notifies the user of the current status of the download.
|
||||||
* @param status Current [DownloadStatus]
|
* @param status Current [DownloadStatus]
|
||||||
* @param dID ID of the notification, defaults to hashCode of the download's packageName
|
|
||||||
*/
|
*/
|
||||||
private suspend fun notifyStatus(status: DownloadStatus) {
|
private suspend fun notifyStatus(status: DownloadStatus) {
|
||||||
// Update status in database
|
// Update status in database
|
||||||
@@ -373,7 +406,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
val notification = NotificationUtil.getDownloadNotification(appContext, download, id, icon)
|
val notification = NotificationUtil.getDownloadNotification(appContext, download, icon)
|
||||||
notificationManager.notify(notificationId, notification)
|
notificationManager.notify(notificationId, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +417,7 @@ class DownloadWorker @AssistedInject constructor(
|
|||||||
@OptIn(ExperimentalStdlibApi::class)
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
private suspend fun verifyFile(gFile: GPlayFile): Boolean {
|
private suspend fun verifyFile(gFile: GPlayFile): Boolean {
|
||||||
val file = PathUtil.getLocalFile(appContext, gFile, download)
|
val file = PathUtil.getLocalFile(appContext, gFile, download)
|
||||||
|
Log.i(TAG, "Verifying $file")
|
||||||
|
|
||||||
val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256
|
val algorithm = if (gFile.sha256.isBlank()) Algorithm.SHA1 else Algorithm.SHA256
|
||||||
val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256
|
val expectedSha = if (algorithm == Algorithm.SHA1) gFile.sha1 else gFile.sha256
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import androidx.core.app.PendingIntentCompat
|
|||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.navigation.NavDeepLinkBuilder
|
import androidx.navigation.NavDeepLinkBuilder
|
||||||
import androidx.work.WorkManager
|
|
||||||
import com.aurora.Constants
|
import com.aurora.Constants
|
||||||
import com.aurora.store.MainActivity
|
import com.aurora.store.MainActivity
|
||||||
import com.aurora.store.R
|
import com.aurora.store.R
|
||||||
import com.aurora.store.data.activity.InstallActivity
|
import com.aurora.store.data.activity.InstallActivity
|
||||||
|
import com.aurora.store.data.helper.DownloadHelper
|
||||||
import com.aurora.store.data.installer.AppInstaller
|
import com.aurora.store.data.installer.AppInstaller
|
||||||
import com.aurora.store.data.model.DownloadStatus
|
import com.aurora.store.data.model.DownloadStatus
|
||||||
|
import com.aurora.store.data.receiver.DownloadCancelReceiver
|
||||||
import com.aurora.store.data.room.download.Download
|
import com.aurora.store.data.room.download.Download
|
||||||
import com.aurora.store.data.room.update.Update
|
import com.aurora.store.data.room.update.Update
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
@@ -88,7 +89,6 @@ object NotificationUtil {
|
|||||||
fun getDownloadNotification(
|
fun getDownloadNotification(
|
||||||
context: Context,
|
context: Context,
|
||||||
download: AuroraDownload,
|
download: AuroraDownload,
|
||||||
workID: UUID,
|
|
||||||
largeIcon: Bitmap? = null
|
largeIcon: Bitmap? = null
|
||||||
): Notification {
|
): Notification {
|
||||||
val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_DOWNLOADS)
|
val builder = NotificationCompat.Builder(context, Constants.NOTIFICATION_CHANNEL_DOWNLOADS)
|
||||||
@@ -96,6 +96,17 @@ object NotificationUtil {
|
|||||||
builder.setContentIntent(getContentIntentForDownloads(context))
|
builder.setContentIntent(getContentIntentForDownloads(context))
|
||||||
builder.setLargeIcon(largeIcon)
|
builder.setLargeIcon(largeIcon)
|
||||||
|
|
||||||
|
val cancelIntent = Intent(context, DownloadCancelReceiver::class.java).apply {
|
||||||
|
putExtra(DownloadHelper.PACKAGE_NAME, download.packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingCancelIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
cancelIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
when (download.downloadStatus) {
|
when (download.downloadStatus) {
|
||||||
DownloadStatus.CANCELLED -> {
|
DownloadStatus.CANCELLED -> {
|
||||||
builder.setSmallIcon(R.drawable.ic_download_cancel)
|
builder.setSmallIcon(R.drawable.ic_download_cancel)
|
||||||
@@ -152,7 +163,7 @@ object NotificationUtil {
|
|||||||
NotificationCompat.Action.Builder(
|
NotificationCompat.Action.Builder(
|
||||||
R.drawable.ic_download_cancel,
|
R.drawable.ic_download_cancel,
|
||||||
context.getString(R.string.action_cancel),
|
context.getString(R.string.action_cancel),
|
||||||
WorkManager.getInstance(context).createCancelPendingIntent(workID)
|
pendingCancelIntent
|
||||||
).build()
|
).build()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user