/*
* Nextcloud Android client application
*
* @author Álvaro Brey
* Copyright (C) 2022 Álvaro Brey
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program. If not, see .
*
*/
package com.nextcloud.client.documentscan
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.nextcloud.client.account.CurrentAccountProvider
import com.nextcloud.client.di.IoDispatcher
import com.nextcloud.client.files.uploader.FileUploadWorker
import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.logger.Logger
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.helpers.FileOperationsHelper
import com.nextcloud.client.files.uploader.FileUploadHelper
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@Suppress("Detekt.LongParameterList") // satisfied by DI
class DocumentScanViewModel @Inject constructor(
@IoDispatcher private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
app: Application,
private val logger: Logger,
private val backgroundJobManager: BackgroundJobManager,
private val currentAccountProvider: CurrentAccountProvider
) : AndroidViewModel(app) {
init {
logger.d(TAG, "DocumentScanViewModel created")
}
sealed interface UIState {
sealed class BaseState(val pageList: List) : UIState {
val isEmpty: Boolean
get() = pageList.isEmpty()
}
class NormalState(
pageList: List = emptyList(),
val shouldRequestScan: Boolean = false
) : BaseState(pageList)
class RequestExportState(
pageList: List = emptyList(),
val shouldRequestExportType: Boolean = true
) : BaseState(pageList)
object DoneState : UIState
object CanceledState : UIState
}
private var uploadFolder: String? = null
private val initialState = UIState.NormalState(shouldRequestScan = true)
private val _uiState = MutableLiveData(initialState)
val uiState: LiveData
get() = _uiState
/**
* @param result should be the path to the scanned page on the disk
*/
fun onScanPageResult(result: String?) {
logger.d(TAG, "onScanPageResult() called with: result = $result")
val state = _uiState.value
require(state is UIState.NormalState)
viewModelScope.launch(ioDispatcher) {
if (result != null) {
val newPath = renameCapturedImage(result)
val pageList = state.pageList.toMutableList()
pageList.add(newPath)
_uiState.postValue(UIState.NormalState(pageList))
} else {
// result == null means cancellation or error
if (state.isEmpty) {
// close only if no pages have been added yet
_uiState.postValue(UIState.CanceledState)
}
}
}
}
// TODO extract to usecase
private fun renameCapturedImage(originalPath: String): String {
val file = File(originalPath)
val renamedFile =
File(
getApplication().cacheDir.path +
File.separator + FileOperationsHelper.getCapturedImageName()
)
file.renameTo(renamedFile)
return renamedFile.absolutePath
}
fun onScanRequestHandled() {
val state = uiState.value
require(state is UIState.NormalState)
_uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = false))
}
fun onAddPageClicked() {
val state = uiState.value
require(state is UIState.NormalState)
if (!state.shouldRequestScan) {
_uiState.postValue(UIState.NormalState(state.pageList, shouldRequestScan = true))
}
}
fun onClickDone() {
val state = _uiState.value
if (state is UIState.BaseState && !state.isEmpty) {
_uiState.postValue(UIState.RequestExportState(state.pageList))
}
}
fun setUploadFolder(folder: String) {
this.uploadFolder = folder
}
fun onRequestTypeHandled() {
val state = _uiState.value
require(state is UIState.RequestExportState)
_uiState.postValue(UIState.RequestExportState(state.pageList, false))
}
fun onExportTypeSelected(exportType: ExportType) {
val state = _uiState.value
require(state is UIState.RequestExportState)
when (exportType) {
ExportType.PDF -> {
exportToPdf(state.pageList)
}
ExportType.IMAGES -> {
exportToImages(state.pageList)
}
}
_uiState.postValue(UIState.DoneState)
}
private fun exportToPdf(pageList: List) {
val genPath =
getApplication().cacheDir.path + File.separator + FileOperationsHelper.getTimestampedFileName(
".pdf"
)
backgroundJobManager.startPdfGenerateAndUploadWork(
currentAccountProvider.user,
uploadFolder!!,
pageList,
genPath
)
// after job is started, finish the application.
_uiState.postValue(UIState.DoneState)
}
private fun exportToImages(pageList: List) {
val uploadPaths = pageList.map {
uploadFolder + OCFile.PATH_SEPARATOR + File(it).name
}.toTypedArray()
FileUploadHelper().uploadNewFiles(
currentAccountProvider.user,
pageList.toTypedArray(),
uploadPaths,
FileUploadWorker.LOCAL_BEHAVIOUR_DELETE,
true,
UploadFileOperation.CREATED_BY_USER,
false,
false,
NameCollisionPolicy.ASK_USER
)
}
fun onExportCanceled() {
val state = _uiState.value
if (state is UIState.BaseState) {
_uiState.postValue(UIState.NormalState(state.pageList))
}
}
private companion object {
private const val TAG = "DocumentScanViewModel"
}
enum class ExportType {
PDF,
IMAGES
}
}