Skip to content

Commit

Permalink
Merge pull request #293 from aivanovski/feature/add-option-to-copy-db…
Browse files Browse the repository at this point in the history
…-key-into-private-storage

Add "Make a copy and Select" option into built-in File Picker
  • Loading branch information
aivanovski authored Jan 31, 2025
2 parents b46ff7a + ccb5d84 commit 1299f93
Show file tree
Hide file tree
Showing 29 changed files with 722 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_FILE_ACCESS_IS
import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_FILE_IS_NOT_A_DIRECTORY
import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_WRITE_OPERATION_IS_NOT_SUPPORTED
import com.ivanovsky.passnotes.data.entity.OperationError.newFileAccessError
import com.ivanovsky.passnotes.data.entity.OperationError.newFileIsAlreadyExistsError
import com.ivanovsky.passnotes.data.entity.OperationError.newFileNotFoundError
import com.ivanovsky.passnotes.data.entity.OperationError.newGenericIOError
import com.ivanovsky.passnotes.data.entity.OperationError.newPermissionError
Expand All @@ -23,6 +24,7 @@ import com.ivanovsky.passnotes.data.repository.file.FileSystemSyncProcessor
import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy
import com.ivanovsky.passnotes.domain.PermissionHelper
import com.ivanovsky.passnotes.domain.entity.SystemPermission
import com.ivanovsky.passnotes.extensions.getOrThrow
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
Expand All @@ -32,6 +34,7 @@ import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.util.concurrent.locks.ReentrantLock
import okio.withLock
import timber.log.Timber

class RegularFileSystemProvider(
Expand Down Expand Up @@ -105,9 +108,11 @@ class RegularFileSystemProvider(
FSType.INTERNAL_STORAGE -> {
getInternalRoot()
}

FSType.EXTERNAL_STORAGE -> {
getExternalRoots().firstOrNull()
}

else -> {
throw IllegalStateException()
}
Expand Down Expand Up @@ -162,18 +167,24 @@ class RegularFileSystemProvider(
return check.takeError()
}

lock.lock()
return try {
val out = BufferedOutputStream(FileOutputStream(file.path))
OperationResult.success(out)
} catch (e: FileNotFoundException) {
Timber.d(e)
OperationResult.error(newGenericIOError(e.message, e))
} catch (e: Exception) {
Timber.d(e)
OperationResult.error(newGenericIOError(e.message, e))
} finally {
lock.unlock()
return lock.withLock {
val isExistResult = exists(file)
if (isExistResult.isFailed) {
return@withLock isExistResult.takeError()
}

val isExist = isExistResult.getOrThrow()
if (isExist && onConflictStrategy == OnConflictStrategy.CANCEL) {
return@withLock OperationResult.error(newFileIsAlreadyExistsError())
}

try {
val out = BufferedOutputStream(FileOutputStream(file.path))
OperationResult.success(out)
} catch (e: Exception) {
Timber.d(e)
OperationResult.error(newGenericIOError(e.message, e))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package com.ivanovsky.passnotes.domain.interactor.filepicker

import com.ivanovsky.passnotes.data.entity.FSAuthority
import com.ivanovsky.passnotes.data.entity.FileDescriptor
import com.ivanovsky.passnotes.data.entity.OperationResult
import com.ivanovsky.passnotes.data.repository.file.FSOptions
import com.ivanovsky.passnotes.data.repository.file.FileSystemResolver
import com.ivanovsky.passnotes.data.repository.file.OnConflictStrategy
import com.ivanovsky.passnotes.domain.DispatcherProvider
import com.ivanovsky.passnotes.extensions.getOrThrow
import com.ivanovsky.passnotes.extensions.mapError
import com.ivanovsky.passnotes.util.InputOutputUtils
import java.io.InputStream
import kotlinx.coroutines.withContext

class FilePickerInteractor(
Expand All @@ -24,4 +31,61 @@ class FilePickerInteractor(
.resolveProvider(file.fsAuthority)
.getParent(file)
}

suspend fun copyToPrivateStorage(file: FileDescriptor): OperationResult<FileDescriptor> =
withContext(dispatchers.IO) {
val inputResult = openFile(file)
if (inputResult.isFailed) {
return@withContext inputResult.mapError()
}

val fsProvider = fileSystemResolver.resolveProvider(FSAuthority.INTERNAL_FS_AUTHORITY)

val getRootResult = fsProvider.rootFile
if (getRootResult.isFailed) {
return@withContext getRootResult.mapError()
}

val root = getRootResult.getOrThrow()
val path = root.path + "/" + file.name
val dstFile = FileDescriptor(
fsAuthority = FSAuthority.INTERNAL_FS_AUTHORITY,
path = path,
uid = path,
name = file.name,
isDirectory = false,
isRoot = false
)

val outputResult = fsProvider.openFileForWrite(
dstFile,
OnConflictStrategy.CANCEL,
FSOptions.DEFAULT
)
if (outputResult.isFailed) {
return@withContext outputResult.mapError()
}

val copyResult = InputOutputUtils.copy(
from = inputResult.getOrThrow(),
to = outputResult.getOrThrow(),
isClose = true
)
if (copyResult.isFailed) {
return@withContext copyResult.mapError()
}

fsProvider.getFile(path, FSOptions.DEFAULT)
}

private suspend fun openFile(file: FileDescriptor): OperationResult<InputStream> =
withContext(dispatchers.IO) {
val fsProvider = fileSystemResolver.resolveProvider(file.fsAuthority)

fsProvider.openFileForRead(
file,
OnConflictStrategy.CANCEL,
FSOptions.READ_ONLY
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import com.ivanovsky.passnotes.domain.interactor.syncState.SyncStateInteractor
import com.ivanovsky.passnotes.domain.interactor.unlock.UnlockInteractor
import com.ivanovsky.passnotes.presentation.about.AboutViewModel
import com.ivanovsky.passnotes.presentation.autofill.AutofillViewFactory
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.OptionDialogArgs
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.OptionDialogViewModel
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.factory.OptionDialogCellModelFactory
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.factory.OptionDialogCellViewModelFactory
import com.ivanovsky.passnotes.presentation.core.dialog.propertyAction.PropertyActionDialogArgs
import com.ivanovsky.passnotes.presentation.core.dialog.propertyAction.PropertyActionDialogViewModel
import com.ivanovsky.passnotes.presentation.core.dialog.resolveConflict.ResolveConflictDialogArgs
Expand Down Expand Up @@ -206,6 +210,9 @@ object UiModule {
single { HistoryCellModelFactory(get(), get()) }
single { HistoryCellViewModelFactory(get()) }

single { OptionDialogCellModelFactory(get()) }
single { OptionDialogCellViewModelFactory(get()) }

// Cicerone
single { Cicerone.create() }
single { provideCiceroneRouter(get()) }
Expand Down Expand Up @@ -382,6 +389,13 @@ object UiModule {
factory { (args: PropertyActionDialogArgs) ->
PropertyActionDialogViewModel(get(), args)
}
factory { (args: OptionDialogArgs) ->
OptionDialogViewModel(
get(),
get(),
args
)
}
}

private fun provideCiceroneRouter(cicerone: Cicerone<Router>) =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.ivanovsky.passnotes.presentation.core.dialog.optionDialog

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.ViewModelProvider
import com.ivanovsky.passnotes.R
import com.ivanovsky.passnotes.databinding.CoreBaseCellDialogBinding
import com.ivanovsky.passnotes.extensions.cloneInContext
import com.ivanovsky.passnotes.presentation.core.ViewModelTypes
import com.ivanovsky.passnotes.presentation.core.adapter.ViewModelsAdapter
import com.ivanovsky.passnotes.presentation.core.extensions.getMandatoryArgument
import com.ivanovsky.passnotes.presentation.core.extensions.setViewModels
import com.ivanovsky.passnotes.presentation.core.extensions.withArguments
import com.ivanovsky.passnotes.presentation.core.viewmodel.DividerCellViewModel
import com.ivanovsky.passnotes.presentation.core.viewmodel.OneLineTextCellViewModel
import com.ivanovsky.passnotes.presentation.core.viewmodel.TwoLineTextCellViewModel

class OptionDialog : DialogFragment() {

private val args: OptionDialogArgs by lazy {
getMandatoryArgument(ARGUMENTS)
}

private val viewModel: OptionDialogViewModel by lazy {
ViewModelProvider(
owner = this,
factory = OptionDialogViewModel.Factory(
args = args
)
)[OptionDialogViewModel::class.java]
}

private lateinit var binding: CoreBaseCellDialogBinding
private var onItemClick: ((index: Int) -> Unit)? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (savedInstanceState != null) {
dismiss()
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val themedInflater = inflater.cloneInContext(R.style.AppDialogTheme)
val binding = CoreBaseCellDialogBinding.inflate(themedInflater, container, false)
.also {
binding = it
}

binding.recyclerView.adapter = ViewModelsAdapter(
lifecycleOwner = viewLifecycleOwner,
viewTypes = ViewModelTypes()
.add(OneLineTextCellViewModel::class, R.layout.cell_option_one_line)
.add(TwoLineTextCellViewModel::class, R.layout.cell_option_two_line)
.add(DividerCellViewModel::class, R.layout.cell_divider)
)

return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

subscribeToLiveData()
subscribeToEvents()
}

private fun subscribeToLiveData() {
viewModel.cellViewModels.observe(viewLifecycleOwner) { viewModels ->
binding.recyclerView.setViewModels(viewModels)
}
}

private fun subscribeToEvents() {
viewModel.selectItemEvent.observe(viewLifecycleOwner) { index ->
onItemClick?.invoke(index)
dismiss()
}
}

companion object {

val TAG = OptionDialog::class.java.simpleName

private const val ARGUMENTS = "arguments"

fun newInstance(
args: OptionDialogArgs,
onItemClick: (index: Int) -> Unit
): OptionDialog =
OptionDialog()
.withArguments {
putParcelable(ARGUMENTS, args)
}
.apply {
this.onItemClick = onItemClick
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ivanovsky.passnotes.presentation.core.dialog.optionDialog

import android.os.Parcelable
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.model.OptionItem
import kotlinx.parcelize.Parcelize

@Parcelize
data class OptionDialogArgs(
val options: List<OptionItem>
) : Parcelable
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.ivanovsky.passnotes.presentation.core.dialog.optionDialog

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.ivanovsky.passnotes.injection.GlobalInjector
import com.ivanovsky.passnotes.presentation.core.BaseCellViewModel
import com.ivanovsky.passnotes.presentation.core.BaseScreenViewModel
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.factory.OptionDialogCellModelFactory
import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.factory.OptionDialogCellViewModelFactory
import com.ivanovsky.passnotes.presentation.core.event.SingleLiveEvent
import com.ivanovsky.passnotes.presentation.core.viewmodel.OneLineTextCellViewModel
import com.ivanovsky.passnotes.presentation.core.viewmodel.TwoLineTextCellViewModel
import com.ivanovsky.passnotes.util.toIntSafely
import org.koin.core.parameter.parametersOf

class OptionDialogViewModel(
private val modelFactory: OptionDialogCellModelFactory,
private val viewModelFactory: OptionDialogCellViewModelFactory,
private val args: OptionDialogArgs
) : BaseScreenViewModel() {

val selectItemEvent = SingleLiveEvent<Int>()

init {
setCellElements(createCellViewModels())
subscribeToEvents()
}

override fun onCleared() {
super.onCleared()
unsubscribeFromEvents()
}

private fun subscribeToEvents() {
eventProvider.subscribe(this) { event ->
event.getString(OneLineTextCellViewModel.CLICK_EVENT)
?.let { cellId ->
onCellClicked(cellId)
}

event.getString(TwoLineTextCellViewModel.CLICK_EVENT)
?.let { cellId ->
onCellClicked(cellId)
}
}
}

private fun onCellClicked(cellId: String) {
val index = cellId.toIntSafely() ?: return

selectItemEvent.call(index)
}

private fun unsubscribeFromEvents() {
eventProvider.unSubscribe(this)
}

private fun createCellViewModels(): List<BaseCellViewModel> {
val models = modelFactory.createCellModels(args.options)
return viewModelFactory.createCellViewModels(models, eventProvider)
}

class Factory(
private val args: OptionDialogArgs
) : ViewModelProvider.Factory {

@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return GlobalInjector.get<OptionDialogViewModel>(
parametersOf(args)
) as T
}
}
}
Loading

0 comments on commit 1299f93

Please sign in to comment.