repo created
This commit is contained in:
commit
1ef725ef20
2483 changed files with 278273 additions and 0 deletions
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.lib.resources.files.FileUtils
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.ui.notifications.NotificationUtils
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.io.File
|
||||
import java.security.SecureRandom
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class DownloadNotificationManager(
|
||||
private val id: Int,
|
||||
private val context: Context,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) {
|
||||
private var notification: Notification
|
||||
private var notificationBuilder: NotificationCompat.Builder
|
||||
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
init {
|
||||
notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
|
||||
setContentTitle(context.getString(R.string.downloader_download_in_progress_ticker))
|
||||
setTicker(context.getString(R.string.downloader_download_in_progress_ticker))
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
|
||||
}
|
||||
}
|
||||
|
||||
notification = notificationBuilder.build()
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun prepareForStart(operation: DownloadFileOperation) {
|
||||
notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
|
||||
setSmallIcon(R.drawable.notification_icon)
|
||||
setOngoing(true)
|
||||
setProgress(100, 0, operation.size < 0)
|
||||
setContentText(
|
||||
String.format(
|
||||
context.getString(R.string.downloader_download_in_progress), 0,
|
||||
File(operation.savePath).name
|
||||
)
|
||||
)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
|
||||
}
|
||||
|
||||
notificationManager.notify(
|
||||
id,
|
||||
this.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun prepareForResult() {
|
||||
notificationBuilder
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(false)
|
||||
.setProgress(0, 0, false)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun updateDownloadProgress(filePath: String, percent: Int, totalToTransfer: Long) {
|
||||
notificationBuilder.run {
|
||||
setProgress(100, percent, totalToTransfer < 0)
|
||||
val fileName: String = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1)
|
||||
val text =
|
||||
String.format(context.getString(R.string.downloader_download_in_progress), percent, fileName)
|
||||
val title =
|
||||
context.getString(R.string.downloader_download_in_progress_ticker)
|
||||
updateNotificationText(title, text)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun dismissNotification() {
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
notificationManager.cancel(id)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
fun showNewNotification(text: String) {
|
||||
val notifyId = SecureRandom().nextInt()
|
||||
|
||||
notificationBuilder.run {
|
||||
setProgress(0, 0, false)
|
||||
setContentTitle(null)
|
||||
setContentText(text)
|
||||
setOngoing(false)
|
||||
notificationManager.notify(notifyId, this.build())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateNotificationText(title: String?, text: String) {
|
||||
notificationBuilder.run {
|
||||
title?.let {
|
||||
setContentTitle(title)
|
||||
}
|
||||
|
||||
setContentText(text)
|
||||
notificationManager.notify(id, this.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentIntent(intent: Intent, flag: Int) {
|
||||
notificationBuilder.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
intent,
|
||||
flag
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getId(): Int {
|
||||
return id
|
||||
}
|
||||
|
||||
fun getNotification(): Notification {
|
||||
return notificationBuilder.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import com.nextcloud.client.core.IsCancelled
|
||||
import com.nextcloud.client.files.DownloadRequest
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.lib.common.OwnCloudClient
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This runnable object encapsulates file download logic. It has been extracted to wrap
|
||||
* network operation and storage manager interactions, as those pose testing challenges
|
||||
* that cannot be addressed due to large number of dependencies.
|
||||
*
|
||||
* This design can be regarded as intermediary refactoring step.
|
||||
*/
|
||||
class DownloadTask(
|
||||
private val context: Context,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val clientProvider: () -> OwnCloudClient
|
||||
) {
|
||||
|
||||
data class Result(val file: OCFile, val success: Boolean)
|
||||
|
||||
/**
|
||||
* This class is a helper factory to to keep static dependencies
|
||||
* injection out of the downloader instance.
|
||||
*
|
||||
* @param context Context
|
||||
* @param clientProvider Provide client - this must be called on background thread
|
||||
* @param contentResolver content resovler used to access file storage
|
||||
*/
|
||||
class Factory(
|
||||
private val context: Context,
|
||||
private val clientProvider: () -> OwnCloudClient,
|
||||
private val contentResolver: ContentResolver
|
||||
) {
|
||||
fun create(): DownloadTask {
|
||||
return DownloadTask(context, contentResolver, clientProvider)
|
||||
}
|
||||
}
|
||||
|
||||
// Unused progress, isCancelled arguments needed for TransferManagerTest
|
||||
fun download(request: DownloadRequest, progress: (Int) -> Unit, isCancelled: IsCancelled): Result {
|
||||
val op = DownloadFileOperation(request.user, request.file, context)
|
||||
val client = clientProvider.invoke()
|
||||
val result = op.execute(client)
|
||||
|
||||
return if (result.isSuccess) {
|
||||
val storageManager = FileDataStorageManager(
|
||||
request.user,
|
||||
contentResolver
|
||||
)
|
||||
val file = saveDownloadedFile(op, storageManager)
|
||||
Result(file, true)
|
||||
} else {
|
||||
Result(request.file, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveDownloadedFile(op: DownloadFileOperation, storageManager: FileDataStorageManager): OCFile {
|
||||
val file = storageManager.getFileById(op.file.fileId) as OCFile
|
||||
|
||||
file.apply {
|
||||
val syncDate = System.currentTimeMillis()
|
||||
lastSyncDateForProperties = syncDate
|
||||
lastSyncDateForData = syncDate
|
||||
isUpdateThumbnailNeeded = true
|
||||
modificationTimestamp = op.modificationTimestamp
|
||||
modificationTimestampAtLastSyncForData = op.modificationTimestamp
|
||||
etag = op.etag
|
||||
mimeType = op.mimeType
|
||||
storagePath = op.savePath
|
||||
fileLength = File(op.savePath).length()
|
||||
remoteId = op.file.remoteId
|
||||
}
|
||||
|
||||
storageManager.saveFile(file)
|
||||
|
||||
if (MimeTypeUtil.isMedia(op.mimeType)) {
|
||||
FileDataStorageManager.triggerMediaScan(file.storagePath)
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
enum class FileDownloadError {
|
||||
Failed, Cancelled
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.jobs.BackgroundJobManager
|
||||
import com.owncloud.android.MainApp
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.datamodel.UploadsStorageManager
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
import com.owncloud.android.utils.MimeTypeUtil
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class FileDownloadHelper {
|
||||
|
||||
@Inject
|
||||
lateinit var backgroundJobManager: BackgroundJobManager
|
||||
|
||||
@Inject
|
||||
lateinit var uploadsStorageManager: UploadsStorageManager
|
||||
|
||||
companion object {
|
||||
private var instance: FileDownloadHelper? = null
|
||||
|
||||
fun instance(): FileDownloadHelper {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: FileDownloadHelper().also { instance = it }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
MainApp.getAppComponent().inject(this)
|
||||
}
|
||||
|
||||
fun isDownloading(user: User?, file: OCFile?): Boolean {
|
||||
if (user == null || file == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
val fileStorageManager = FileDataStorageManager(user, MainApp.getAppContext().contentResolver)
|
||||
val topParentId = fileStorageManager.getTopParentId(file)
|
||||
|
||||
val isJobScheduled = backgroundJobManager.isStartFileDownloadJobScheduled(user, file.fileId)
|
||||
return isJobScheduled || if (file.isFolder) {
|
||||
backgroundJobManager.isStartFileDownloadJobScheduled(user, topParentId)
|
||||
} else {
|
||||
FileDownloadWorker.isDownloading(user.accountName, file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelPendingOrCurrentDownloads(user: User?, files: List<OCFile>?) {
|
||||
if (user == null || files == null) return
|
||||
|
||||
files.forEach { file ->
|
||||
FileDownloadWorker.cancelOperation(user.accountName, file.fileId)
|
||||
backgroundJobManager.cancelFilesDownloadJob(user, file.fileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAllDownloadsForAccount(accountName: String?, currentDownload: DownloadFileOperation?) {
|
||||
if (accountName == null || currentDownload == null) return
|
||||
|
||||
val currentUser = currentDownload.user
|
||||
val currentFile = currentDownload.file
|
||||
|
||||
if (!currentUser.nameEquals(accountName)) {
|
||||
return
|
||||
}
|
||||
|
||||
currentDownload.cancel()
|
||||
FileDownloadWorker.cancelOperation(currentUser.accountName, currentFile.fileId)
|
||||
backgroundJobManager.cancelFilesDownloadJob(currentUser, currentFile.fileId)
|
||||
}
|
||||
|
||||
fun saveFile(
|
||||
file: OCFile,
|
||||
currentDownload: DownloadFileOperation?,
|
||||
storageManager: FileDataStorageManager?
|
||||
) {
|
||||
val syncDate = System.currentTimeMillis()
|
||||
|
||||
file.apply {
|
||||
lastSyncDateForProperties = syncDate
|
||||
lastSyncDateForData = syncDate
|
||||
isUpdateThumbnailNeeded = true
|
||||
modificationTimestamp = currentDownload?.modificationTimestamp ?: 0L
|
||||
modificationTimestampAtLastSyncForData = currentDownload?.modificationTimestamp ?: 0L
|
||||
etag = currentDownload?.etag
|
||||
mimeType = currentDownload?.mimeType
|
||||
storagePath = currentDownload?.savePath
|
||||
|
||||
val savePathFile = currentDownload?.savePath?.let { File(it) }
|
||||
savePathFile?.let {
|
||||
fileLength = savePathFile.length()
|
||||
}
|
||||
|
||||
remoteId = currentDownload?.file?.remoteId
|
||||
}
|
||||
|
||||
storageManager?.saveFile(file)
|
||||
|
||||
if (MimeTypeUtil.isMedia(currentDownload?.mimeType)) {
|
||||
FileDataStorageManager.triggerMediaScan(file.storagePath, file)
|
||||
}
|
||||
|
||||
storageManager?.saveConflict(file, null)
|
||||
}
|
||||
|
||||
fun downloadFileIfNotStartedBefore(user: User, file: OCFile) {
|
||||
if (!isDownloading(user, file)) {
|
||||
downloadFile(user, file, downloadType = DownloadType.DOWNLOAD)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFile(user: User, file: OCFile) {
|
||||
downloadFile(user, file, downloadType = DownloadType.DOWNLOAD)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun downloadFile(
|
||||
user: User,
|
||||
ocFile: OCFile,
|
||||
behaviour: String = "",
|
||||
downloadType: DownloadType? = DownloadType.DOWNLOAD,
|
||||
activityName: String = "",
|
||||
packageName: String = "",
|
||||
conflictUploadId: Long? = null
|
||||
) {
|
||||
backgroundJobManager.startFileDownloadJob(
|
||||
user,
|
||||
ocFile,
|
||||
behaviour,
|
||||
downloadType,
|
||||
activityName,
|
||||
packageName,
|
||||
conflictUploadId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import com.nextcloud.client.account.User
|
||||
import com.owncloud.android.authentication.AuthenticatorActivity
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.ui.activity.FileActivity
|
||||
import com.owncloud.android.ui.activity.FileDisplayActivity
|
||||
import com.owncloud.android.ui.dialog.SendShareDialog
|
||||
import com.owncloud.android.ui.fragment.OCFileListFragment
|
||||
import com.owncloud.android.ui.preview.PreviewImageActivity
|
||||
import com.owncloud.android.ui.preview.PreviewImageFragment
|
||||
|
||||
class FileDownloadIntents(private val context: Context) {
|
||||
|
||||
fun newDownloadIntent(
|
||||
download: DownloadFileOperation,
|
||||
linkedToRemotePath: String
|
||||
): Intent {
|
||||
return Intent(FileDownloadWorker.getDownloadAddedMessage()).apply {
|
||||
putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
|
||||
putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
|
||||
putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, linkedToRemotePath)
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadFinishedIntent(
|
||||
download: DownloadFileOperation,
|
||||
downloadResult: RemoteOperationResult<*>,
|
||||
unlinkedFromRemotePath: String?
|
||||
): Intent {
|
||||
return Intent(FileDownloadWorker.getDownloadFinishMessage()).apply {
|
||||
putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess)
|
||||
putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
|
||||
putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
|
||||
putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour)
|
||||
putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName)
|
||||
putExtra(SendShareDialog.PACKAGE_NAME, download.packageName)
|
||||
if (unlinkedFromRemotePath != null) {
|
||||
putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
|
||||
}
|
||||
setPackage(context.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun credentialContentIntent(user: User): Intent {
|
||||
return Intent(context, AuthenticatorActivity::class.java).apply {
|
||||
putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount())
|
||||
putExtra(
|
||||
AuthenticatorActivity.EXTRA_ACTION,
|
||||
AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
|
||||
)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
|
||||
addFlags(Intent.FLAG_FROM_BACKGROUND)
|
||||
}
|
||||
}
|
||||
|
||||
fun detailsIntent(operation: DownloadFileOperation?): Intent {
|
||||
return if (operation != null) {
|
||||
if (PreviewImageFragment.canBePreviewed(operation.file)) {
|
||||
Intent(context, PreviewImageActivity::class.java)
|
||||
} else {
|
||||
Intent(context, FileDisplayActivity::class.java)
|
||||
}.apply {
|
||||
putExtra(FileActivity.EXTRA_FILE, operation.file)
|
||||
putExtra(FileActivity.EXTRA_USER, operation.user)
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
} else {
|
||||
Intent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,466 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper_ozturk@proton.me>
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
package com.nextcloud.client.jobs.download
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.util.Pair
|
||||
import androidx.core.util.component1
|
||||
import androidx.core.util.component2
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.nextcloud.client.account.User
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.model.WorkerState
|
||||
import com.nextcloud.model.WorkerStateLiveData
|
||||
import com.nextcloud.utils.ForegroundServiceHelper
|
||||
import com.owncloud.android.R
|
||||
import com.owncloud.android.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.datamodel.ForegroundServiceType
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.files.services.IndexedForest
|
||||
import com.owncloud.android.lib.common.OwnCloudAccount
|
||||
import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
|
||||
import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult
|
||||
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
|
||||
import com.owncloud.android.lib.common.utils.Log_OC
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
import com.owncloud.android.operations.DownloadType
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import java.security.SecureRandom
|
||||
import java.util.AbstractList
|
||||
import java.util.Optional
|
||||
import java.util.Vector
|
||||
|
||||
@Suppress("LongParameterList", "TooManyFunctions")
|
||||
class FileDownloadWorker(
|
||||
viewThemeUtils: ViewThemeUtils,
|
||||
private val accountManager: UserAccountManager,
|
||||
private var localBroadcastManager: LocalBroadcastManager,
|
||||
private val context: Context,
|
||||
params: WorkerParameters
|
||||
) : Worker(context, params), OnAccountsUpdateListener, OnDatatransferProgressListener {
|
||||
|
||||
companion object {
|
||||
private val TAG = FileDownloadWorker::class.java.simpleName
|
||||
|
||||
private val pendingDownloads = IndexedForest<DownloadFileOperation>()
|
||||
|
||||
fun cancelOperation(accountName: String, fileId: Long) {
|
||||
pendingDownloads.all.forEach {
|
||||
it.value?.payload?.cancelMatchingOperation(accountName, fileId)
|
||||
}
|
||||
}
|
||||
|
||||
fun isDownloading(accountName: String, fileId: Long): Boolean {
|
||||
return pendingDownloads.all.any { it.value?.payload?.isMatching(accountName, fileId) == true }
|
||||
}
|
||||
|
||||
const val FILE_REMOTE_PATH = "FILE_REMOTE_PATH"
|
||||
const val ACCOUNT_NAME = "ACCOUNT_NAME"
|
||||
const val BEHAVIOUR = "BEHAVIOUR"
|
||||
const val DOWNLOAD_TYPE = "DOWNLOAD_TYPE"
|
||||
const val ACTIVITY_NAME = "ACTIVITY_NAME"
|
||||
const val PACKAGE_NAME = "PACKAGE_NAME"
|
||||
const val CONFLICT_UPLOAD_ID = "CONFLICT_UPLOAD_ID"
|
||||
|
||||
const val EXTRA_DOWNLOAD_RESULT = "EXTRA_DOWNLOAD_RESULT"
|
||||
const val EXTRA_REMOTE_PATH = "EXTRA_REMOTE_PATH"
|
||||
const val EXTRA_LINKED_TO_PATH = "EXTRA_LINKED_TO_PATH"
|
||||
const val EXTRA_ACCOUNT_NAME = "EXTRA_ACCOUNT_NAME"
|
||||
|
||||
fun getDownloadAddedMessage(): String {
|
||||
return FileDownloadWorker::class.java.name + "DOWNLOAD_ADDED"
|
||||
}
|
||||
|
||||
fun getDownloadFinishMessage(): String {
|
||||
return FileDownloadWorker::class.java.name + "DOWNLOAD_FINISH"
|
||||
}
|
||||
}
|
||||
|
||||
private var currentDownload: DownloadFileOperation? = null
|
||||
|
||||
private var conflictUploadId: Long? = null
|
||||
private var lastPercent = 0
|
||||
|
||||
private val intents = FileDownloadIntents(context)
|
||||
private var notificationManager = DownloadNotificationManager(
|
||||
SecureRandom().nextInt(),
|
||||
context,
|
||||
viewThemeUtils
|
||||
)
|
||||
|
||||
private var downloadProgressListener = FileDownloadProgressListener()
|
||||
|
||||
private var user: User? = null
|
||||
private var currentUser = Optional.empty<User>()
|
||||
|
||||
private var currentUserFileStorageManager: FileDataStorageManager? = null
|
||||
private var fileDataStorageManager: FileDataStorageManager? = null
|
||||
|
||||
private var downloadError: FileDownloadError? = null
|
||||
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
override fun doWork(): Result {
|
||||
return try {
|
||||
val requestDownloads = getRequestDownloads()
|
||||
addAccountUpdateListener()
|
||||
|
||||
val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
|
||||
notificationManager.getId(),
|
||||
notificationManager.getNotification(),
|
||||
ForegroundServiceType.DataSync
|
||||
)
|
||||
setForegroundAsync(foregroundInfo)
|
||||
|
||||
requestDownloads.forEach {
|
||||
downloadFile(it)
|
||||
}
|
||||
|
||||
downloadError?.let {
|
||||
showDownloadErrorNotification(it)
|
||||
notificationManager.dismissNotification()
|
||||
}
|
||||
|
||||
setIdleWorkerState()
|
||||
|
||||
Log_OC.e(TAG, "FilesDownloadWorker successfully completed")
|
||||
Result.success()
|
||||
} catch (t: Throwable) {
|
||||
notificationManager.dismissNotification()
|
||||
notificationManager.showNewNotification(context.getString(R.string.downloader_unexpected_error))
|
||||
Log_OC.e(TAG, "Error caught at FilesDownloadWorker(): " + t.localizedMessage)
|
||||
setIdleWorkerState()
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
Log_OC.e(TAG, "FilesDownloadWorker stopped")
|
||||
|
||||
notificationManager.dismissNotification()
|
||||
setIdleWorkerState()
|
||||
|
||||
super.onStopped()
|
||||
}
|
||||
|
||||
private fun setWorkerState(user: User?) {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Download(user, currentDownload))
|
||||
}
|
||||
|
||||
private fun setIdleWorkerState() {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
|
||||
}
|
||||
|
||||
private fun removePendingDownload(accountName: String?) {
|
||||
pendingDownloads.remove(accountName)
|
||||
}
|
||||
|
||||
private fun getRequestDownloads(): AbstractList<String> {
|
||||
setUser()
|
||||
val files = getFiles()
|
||||
val downloadType = getDownloadType()
|
||||
|
||||
conflictUploadId = inputData.keyValueMap[CONFLICT_UPLOAD_ID] as Long?
|
||||
|
||||
val behaviour = inputData.keyValueMap[BEHAVIOUR] as String? ?: ""
|
||||
val activityName = inputData.keyValueMap[ACTIVITY_NAME] as String? ?: ""
|
||||
val packageName = inputData.keyValueMap[PACKAGE_NAME] as String? ?: ""
|
||||
|
||||
val requestedDownloads: AbstractList<String> = Vector()
|
||||
|
||||
return try {
|
||||
files.forEach { file ->
|
||||
val operation = DownloadFileOperation(
|
||||
user,
|
||||
file,
|
||||
behaviour,
|
||||
activityName,
|
||||
packageName,
|
||||
context,
|
||||
downloadType
|
||||
)
|
||||
|
||||
operation.addDownloadDataTransferProgressListener(this)
|
||||
operation.addDownloadDataTransferProgressListener(downloadProgressListener)
|
||||
val (downloadKey, linkedToRemotePath) = pendingDownloads.putIfAbsent(
|
||||
user?.accountName,
|
||||
file.remotePath,
|
||||
operation
|
||||
) ?: Pair(null, null)
|
||||
|
||||
downloadKey?.let {
|
||||
requestedDownloads.add(downloadKey)
|
||||
}
|
||||
|
||||
linkedToRemotePath?.let {
|
||||
localBroadcastManager.sendBroadcast(intents.newDownloadIntent(operation, linkedToRemotePath))
|
||||
}
|
||||
}
|
||||
|
||||
requestedDownloads
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log_OC.e(TAG, "Not enough information provided in intent: " + e.message)
|
||||
requestedDownloads
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUser() {
|
||||
val accountName = inputData.keyValueMap[ACCOUNT_NAME] as String
|
||||
user = accountManager.getUser(accountName).get()
|
||||
fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
|
||||
}
|
||||
|
||||
private fun getFiles(): List<OCFile> {
|
||||
val remotePath = inputData.keyValueMap[FILE_REMOTE_PATH] as String?
|
||||
val file = fileDataStorageManager?.getFileByEncryptedRemotePath(remotePath) ?: return listOf()
|
||||
|
||||
return if (file.isFolder) {
|
||||
fileDataStorageManager?.getAllFilesRecursivelyInsideFolder(file) ?: listOf()
|
||||
} else {
|
||||
listOf(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDownloadType(): DownloadType? {
|
||||
val typeAsString = inputData.keyValueMap[DOWNLOAD_TYPE] as String?
|
||||
return if (typeAsString != null) {
|
||||
if (typeAsString == DownloadType.DOWNLOAD.toString()) {
|
||||
DownloadType.DOWNLOAD
|
||||
} else {
|
||||
DownloadType.EXPORT
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAccountUpdateListener() {
|
||||
val am = AccountManager.get(context)
|
||||
am.addOnAccountsUpdatedListener(this, null, false)
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught", "DEPRECATION")
|
||||
private fun downloadFile(downloadKey: String) {
|
||||
currentDownload = pendingDownloads.get(downloadKey)
|
||||
|
||||
if (currentDownload == null) {
|
||||
return
|
||||
}
|
||||
|
||||
setWorkerState(user)
|
||||
Log_OC.e(TAG, "FilesDownloadWorker downloading: $downloadKey")
|
||||
|
||||
val isAccountExist = accountManager.exists(currentDownload?.user?.toPlatformAccount())
|
||||
if (!isAccountExist) {
|
||||
removePendingDownload(currentDownload?.user?.accountName)
|
||||
return
|
||||
}
|
||||
|
||||
notifyDownloadStart(currentDownload!!)
|
||||
var downloadResult: RemoteOperationResult<*>? = null
|
||||
try {
|
||||
val ocAccount = getOCAccountForDownload()
|
||||
val downloadClient =
|
||||
OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
|
||||
|
||||
downloadResult = currentDownload?.execute(downloadClient)
|
||||
if (downloadResult?.isSuccess == true && currentDownload?.downloadType === DownloadType.DOWNLOAD) {
|
||||
getCurrentFile()?.let {
|
||||
FileDownloadHelper.instance().saveFile(it, currentDownload, currentUserFileStorageManager)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log_OC.e(TAG, "Error downloading", e)
|
||||
downloadResult = RemoteOperationResult<Any?>(e)
|
||||
} finally {
|
||||
cleanupDownloadProcess(downloadResult)
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyDownloadStart(download: DownloadFileOperation) {
|
||||
lastPercent = 0
|
||||
|
||||
notificationManager.run {
|
||||
prepareForStart(download)
|
||||
setContentIntent(intents.detailsIntent(download), PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getOCAccountForDownload(): OwnCloudAccount {
|
||||
val currentDownloadAccount = currentDownload?.user?.toPlatformAccount()
|
||||
val currentDownloadUser = accountManager.getUser(currentDownloadAccount?.name)
|
||||
if (currentUser != currentDownloadUser) {
|
||||
currentUser = currentDownloadUser
|
||||
currentUserFileStorageManager = FileDataStorageManager(currentUser.get(), context.contentResolver)
|
||||
}
|
||||
return currentDownloadUser.get().toOwnCloudAccount()
|
||||
}
|
||||
|
||||
private fun getCurrentFile(): OCFile? {
|
||||
var file: OCFile? = currentDownload?.file?.fileId?.let { currentUserFileStorageManager?.getFileById(it) }
|
||||
|
||||
if (file == null) {
|
||||
file = currentUserFileStorageManager?.getFileByDecryptedRemotePath(currentDownload?.file?.remotePath)
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
Log_OC.e(this, "Could not save " + currentDownload?.file?.remotePath)
|
||||
return null
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
private fun cleanupDownloadProcess(result: RemoteOperationResult<*>?) {
|
||||
result?.let {
|
||||
checkDownloadError(it)
|
||||
}
|
||||
|
||||
val removeResult = pendingDownloads.removePayload(
|
||||
currentDownload?.user?.accountName,
|
||||
currentDownload?.remotePath
|
||||
)
|
||||
|
||||
val downloadResult = result ?: RemoteOperationResult<Any?>(RuntimeException("Error downloading…"))
|
||||
|
||||
currentDownload?.run {
|
||||
notifyDownloadResult(this, downloadResult)
|
||||
|
||||
val downloadFinishedIntent = intents.downloadFinishedIntent(
|
||||
this,
|
||||
downloadResult,
|
||||
removeResult.second
|
||||
)
|
||||
|
||||
localBroadcastManager.sendBroadcast(downloadFinishedIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkDownloadError(result: RemoteOperationResult<*>) {
|
||||
if (result.isSuccess || downloadError != null) {
|
||||
return
|
||||
}
|
||||
|
||||
downloadError = if (result.isCancelled) {
|
||||
FileDownloadError.Cancelled
|
||||
} else {
|
||||
FileDownloadError.Failed
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDownloadErrorNotification(downloadError: FileDownloadError) {
|
||||
val text = when (downloadError) {
|
||||
FileDownloadError.Cancelled -> {
|
||||
context.getString(R.string.downloader_file_download_cancelled)
|
||||
}
|
||||
FileDownloadError.Failed -> {
|
||||
context.getString(R.string.downloader_file_download_failed)
|
||||
}
|
||||
}
|
||||
|
||||
notificationManager.showNewNotification(text)
|
||||
}
|
||||
|
||||
private fun notifyDownloadResult(
|
||||
download: DownloadFileOperation,
|
||||
downloadResult: RemoteOperationResult<*>
|
||||
) {
|
||||
if (downloadResult.isCancelled) {
|
||||
return
|
||||
}
|
||||
|
||||
val needsToUpdateCredentials = (ResultCode.UNAUTHORIZED == downloadResult.code)
|
||||
notificationManager.run {
|
||||
prepareForResult()
|
||||
|
||||
if (needsToUpdateCredentials) {
|
||||
showNewNotification(context.getString(R.string.downloader_download_failed_credentials_error))
|
||||
setContentIntent(
|
||||
intents.credentialContentIntent(download.user),
|
||||
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
} else {
|
||||
setContentIntent(intents.detailsIntent(null), PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
override fun onAccountsUpdated(accounts: Array<out Account>?) {
|
||||
if (!accountManager.exists(currentDownload?.user?.toPlatformAccount())) {
|
||||
currentDownload?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override fun onTransferProgress(
|
||||
progressRate: Long,
|
||||
totalTransferredSoFar: Long,
|
||||
totalToTransfer: Long,
|
||||
filePath: String
|
||||
) {
|
||||
val percent: Int = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
|
||||
|
||||
if (percent != lastPercent) {
|
||||
notificationManager.run {
|
||||
updateDownloadProgress(filePath, percent, totalToTransfer)
|
||||
}
|
||||
}
|
||||
|
||||
lastPercent = percent
|
||||
}
|
||||
|
||||
inner class FileDownloadProgressListener : OnDatatransferProgressListener {
|
||||
private val boundListeners: MutableMap<Long, OnDatatransferProgressListener> = HashMap()
|
||||
|
||||
fun isDownloading(user: User?, file: OCFile?): Boolean {
|
||||
return FileDownloadHelper.instance().isDownloading(user, file)
|
||||
}
|
||||
|
||||
fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
|
||||
if (file == null || listener == null) {
|
||||
return
|
||||
}
|
||||
|
||||
boundListeners[file.fileId] = listener
|
||||
}
|
||||
|
||||
fun removeDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
|
||||
if (file == null || listener == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val fileId = file.fileId
|
||||
if (boundListeners[fileId] === listener) {
|
||||
boundListeners.remove(fileId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTransferProgress(
|
||||
progressRate: Long,
|
||||
totalTransferredSoFar: Long,
|
||||
totalToTransfer: Long,
|
||||
fileName: String
|
||||
) {
|
||||
val listener = boundListeners[currentDownload?.file?.fileId]
|
||||
listener?.onTransferProgress(
|
||||
progressRate,
|
||||
totalTransferredSoFar,
|
||||
totalToTransfer,
|
||||
fileName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue