diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt index 90629edd..29026e45 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/data/repository/file/regular/RegularFileSystemProvider.kt @@ -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 @@ -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 @@ -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( @@ -105,9 +108,11 @@ class RegularFileSystemProvider( FSType.INTERNAL_STORAGE -> { getInternalRoot() } + FSType.EXTERNAL_STORAGE -> { getExternalRoots().firstOrNull() } + else -> { throw IllegalStateException() } @@ -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)) + } } } diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt index e21630d9..f375d061 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/domain/interactor/filepicker/FilePickerInteractor.kt @@ -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( @@ -24,4 +31,61 @@ class FilePickerInteractor( .resolveProvider(file.fsAuthority) .getParent(file) } + + suspend fun copyToPrivateStorage(file: FileDescriptor): OperationResult = + 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 = + withContext(dispatchers.IO) { + val fsProvider = fileSystemResolver.resolveProvider(file.fsAuthority) + + fsProvider.openFileForRead( + file, + OnConflictStrategy.CANCEL, + FSOptions.READ_ONLY + ) + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt index 202c2048..a72bd8ec 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/injection/modules/UiModule.kt @@ -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 @@ -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()) } @@ -382,6 +389,13 @@ object UiModule { factory { (args: PropertyActionDialogArgs) -> PropertyActionDialogViewModel(get(), args) } + factory { (args: OptionDialogArgs) -> + OptionDialogViewModel( + get(), + get(), + args + ) + } } private fun provideCiceroneRouter(cicerone: Cicerone) = diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialog.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialog.kt new file mode 100644 index 00000000..d311a3bc --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialog.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogArgs.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogArgs.kt new file mode 100644 index 00000000..aa6813a1 --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogArgs.kt @@ -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 +) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogViewModel.kt new file mode 100644 index 00000000..15bf2fcf --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/OptionDialogViewModel.kt @@ -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() + + 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 { + val models = modelFactory.createCellModels(args.options) + return viewModelFactory.createCellViewModels(models, eventProvider) + } + + class Factory( + private val args: OptionDialogArgs + ) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return GlobalInjector.get( + parametersOf(args) + ) as T + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellModelFactory.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellModelFactory.kt new file mode 100644 index 00000000..24c9b8eb --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellModelFactory.kt @@ -0,0 +1,49 @@ +package com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.factory + +import com.ivanovsky.passnotes.R +import com.ivanovsky.passnotes.domain.ResourceProvider +import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.model.OptionItem +import com.ivanovsky.passnotes.presentation.core.factory.CellModelFactory +import com.ivanovsky.passnotes.presentation.core.model.BaseCellModel +import com.ivanovsky.passnotes.presentation.core.model.DividerCellModel +import com.ivanovsky.passnotes.presentation.core.model.OneLineTextCellModel +import com.ivanovsky.passnotes.presentation.core.model.TwoLineTextCellModel + +class OptionDialogCellModelFactory( + private val resourceProvider: ResourceProvider +) : CellModelFactory> { + + override fun createCellModels(data: List): List { + val models = mutableListOf() + + for ((index, item) in data.withIndex()) { + if (index > 0) { + models.add(createDividerCell()) + } + + val model = if (!item.description.isNullOrEmpty()) { + TwoLineTextCellModel( + id = index.toString(), + title = item.title, + description = item.description + ) + } else { + OneLineTextCellModel( + id = index.toString(), + text = item.title + ) + } + + models.add(model) + } + + return models + } + + private fun createDividerCell(): DividerCellModel = + DividerCellModel( + color = resourceProvider.getAttributeColor(R.attr.kpDividerColor), + paddingStart = null, + paddingEnd = null + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellViewModelFactory.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellViewModelFactory.kt new file mode 100644 index 00000000..135be041 --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/factory/OptionDialogCellViewModelFactory.kt @@ -0,0 +1,29 @@ +package com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.factory + +import com.ivanovsky.passnotes.domain.ResourceProvider +import com.ivanovsky.passnotes.presentation.core.BaseCellViewModel +import com.ivanovsky.passnotes.presentation.core.event.EventProvider +import com.ivanovsky.passnotes.presentation.core.factory.CellViewModelFactory +import com.ivanovsky.passnotes.presentation.core.model.BaseCellModel +import com.ivanovsky.passnotes.presentation.core.model.DividerCellModel +import com.ivanovsky.passnotes.presentation.core.model.OneLineTextCellModel +import com.ivanovsky.passnotes.presentation.core.model.TwoLineTextCellModel +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 OptionDialogCellViewModelFactory( + private val resourceProvider: ResourceProvider +) : CellViewModelFactory { + + override fun createCellViewModel( + model: BaseCellModel, + eventProvider: EventProvider + ): BaseCellViewModel = + when (model) { + is OneLineTextCellModel -> OneLineTextCellViewModel(model, eventProvider) + is TwoLineTextCellModel -> TwoLineTextCellViewModel(model, eventProvider) + is DividerCellModel -> DividerCellViewModel(model, resourceProvider) + else -> throwUnsupportedModelException(model) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/model/OptionItem.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/model/OptionItem.kt new file mode 100644 index 00000000..e6ece98e --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/dialog/optionDialog/model/OptionItem.kt @@ -0,0 +1,10 @@ +package com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class OptionItem( + val title: String, + val description: String? +) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/SingleTextCellModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/OneLineTextCellModel.kt similarity index 79% rename from app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/SingleTextCellModel.kt rename to app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/OneLineTextCellModel.kt index c3c9274f..0aebc107 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/SingleTextCellModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/OneLineTextCellModel.kt @@ -1,6 +1,6 @@ package com.ivanovsky.passnotes.presentation.core.model -data class SingleTextCellModel( +data class OneLineTextCellModel( override val id: String?, val text: String ) : BaseCellModel() \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/TwoLineTextCellModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/TwoLineTextCellModel.kt new file mode 100644 index 00000000..b84e6f4c --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/model/TwoLineTextCellModel.kt @@ -0,0 +1,7 @@ +package com.ivanovsky.passnotes.presentation.core.model + +data class TwoLineTextCellModel( + override val id: String?, + val title: String, + val description: String +) : BaseCellModel(id) \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/FileCellViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/FileCellViewModel.kt index ca3ab08b..e2c3e4ef 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/FileCellViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/FileCellViewModel.kt @@ -14,8 +14,12 @@ class FileCellViewModel( eventProvider.send((CLICK_EVENT to model.id).toEvent()) } - companion object { + fun onLongClicked() { + eventProvider.send((LONG_CLICK_EVENT to model.id).toEvent()) + } + companion object { val CLICK_EVENT = FileCellViewModel::class.qualifiedName + "_clickEvent" + val LONG_CLICK_EVENT = FileCellViewModel::class.qualifiedName + "_longClickEvent" } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/SingleTextCellViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/OneLineTextCellViewModel.kt similarity index 66% rename from app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/SingleTextCellViewModel.kt rename to app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/OneLineTextCellViewModel.kt index d4bb6c85..ff0ecad7 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/SingleTextCellViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/OneLineTextCellViewModel.kt @@ -3,10 +3,10 @@ package com.ivanovsky.passnotes.presentation.core.viewmodel import com.ivanovsky.passnotes.presentation.core.BaseCellViewModel import com.ivanovsky.passnotes.presentation.core.event.Event.Companion.toEvent import com.ivanovsky.passnotes.presentation.core.event.EventProvider -import com.ivanovsky.passnotes.presentation.core.model.SingleTextCellModel +import com.ivanovsky.passnotes.presentation.core.model.OneLineTextCellModel -class SingleTextCellViewModel( - override val model: SingleTextCellModel, +class OneLineTextCellViewModel( + override val model: OneLineTextCellModel, private val eventProvider: EventProvider ) : BaseCellViewModel(model) { @@ -16,6 +16,6 @@ class SingleTextCellViewModel( companion object { - val CLICK_EVENT = SingleTextCellModel::class.qualifiedName + "_clickEvent" + val CLICK_EVENT = OneLineTextCellViewModel::class.qualifiedName + "_clickEvent" } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/TwoLineTextCellViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/TwoLineTextCellViewModel.kt new file mode 100644 index 00000000..7e2b9c23 --- /dev/null +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/core/viewmodel/TwoLineTextCellViewModel.kt @@ -0,0 +1,21 @@ +package com.ivanovsky.passnotes.presentation.core.viewmodel + +import com.ivanovsky.passnotes.presentation.core.BaseCellViewModel +import com.ivanovsky.passnotes.presentation.core.event.Event.Companion.toEvent +import com.ivanovsky.passnotes.presentation.core.event.EventProvider +import com.ivanovsky.passnotes.presentation.core.model.TwoLineTextCellModel + +class TwoLineTextCellViewModel( + override val model: TwoLineTextCellModel, + private val eventProvider: EventProvider +) : BaseCellViewModel(model) { + + fun onClicked() { + eventProvider.send((CLICK_EVENT to model.id).toEvent()) + } + + companion object { + + val CLICK_EVENT = TwoLineTextCellViewModel::class.qualifiedName + "_clickEvent" + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt index 0d6fb932..59a0e7cc 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerFragment.kt @@ -15,6 +15,9 @@ import com.ivanovsky.passnotes.injection.GlobalInjector.inject import com.ivanovsky.passnotes.presentation.core.FragmentWithDoneButton import com.ivanovsky.passnotes.presentation.core.adapter.ViewModelsAdapter import com.ivanovsky.passnotes.presentation.core.dialog.AllFilesPermissionDialog +import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.OptionDialog +import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.OptionDialogArgs +import com.ivanovsky.passnotes.presentation.core.dialog.optionDialog.model.OptionItem import com.ivanovsky.passnotes.presentation.core.extensions.getMandatoryArgument import com.ivanovsky.passnotes.presentation.core.extensions.requestSystemPermission import com.ivanovsky.passnotes.presentation.core.extensions.setViewModels @@ -24,6 +27,7 @@ import com.ivanovsky.passnotes.presentation.core.extensions.withArguments import com.ivanovsky.passnotes.presentation.core.permission.PermissionRequestResultReceiver import com.ivanovsky.passnotes.presentation.filepicker.Action.PICK_DIRECTORY import com.ivanovsky.passnotes.presentation.filepicker.Action.PICK_FILE +import com.ivanovsky.passnotes.presentation.filepicker.FilePickerViewModel.FileMenuItem import timber.log.Timber class FilePickerFragment : @@ -144,6 +148,9 @@ class FilePickerFragment : viewModel.showAllFilePermissionDialogEvent.observe(viewLifecycleOwner) { showAllFilePermissionDialog() } + viewModel.showFileMenuDialog.observe(viewLifecycleOwner) { items -> + showFileMenuDialog(items) + } } private fun showAllFilePermissionDialog() { @@ -159,6 +166,30 @@ class FilePickerFragment : dialog.show(childFragmentManager, AllFilesPermissionDialog.TAG) } + private fun showFileMenuDialog(items: List) { + val options = items.map { item -> + when (item) { + FileMenuItem.SELECT -> OptionItem( + title = resources.getString(R.string.select), + description = null + ) + + FileMenuItem.COPY_AND_SELECT -> OptionItem( + title = resources.getString(R.string.make_a_copy_and_select), + description = resources.getString(R.string.copy_and_select_description) + ) + } + } + + val dialog = OptionDialog.newInstance( + args = OptionDialogArgs(options), + onItemClick = { index -> + viewModel.onFileMenuClicked(items[index]) + } + ) + dialog.show(childFragmentManager, OptionDialog.TAG) + } + companion object { private const val ARGUMENTS = "arguments" diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt index f5175b96..c8756b97 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/filepicker/FilePickerViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.github.terrakok.cicerone.Router import com.ivanovsky.passnotes.R +import com.ivanovsky.passnotes.data.entity.FSType import com.ivanovsky.passnotes.data.entity.FileDescriptor import com.ivanovsky.passnotes.data.entity.OperationResult import com.ivanovsky.passnotes.data.repository.file.AuthType @@ -16,6 +17,7 @@ import com.ivanovsky.passnotes.domain.ResourceProvider import com.ivanovsky.passnotes.domain.entity.SystemPermission import com.ivanovsky.passnotes.domain.interactor.ErrorInteractor import com.ivanovsky.passnotes.domain.interactor.filepicker.FilePickerInteractor +import com.ivanovsky.passnotes.extensions.getOrThrow import com.ivanovsky.passnotes.injection.GlobalInjector import com.ivanovsky.passnotes.presentation.Screens.FilePickerScreen import com.ivanovsky.passnotes.presentation.core.BaseScreenViewModel @@ -56,6 +58,7 @@ class FilePickerViewModel( val showAllFilePermissionDialogEvent = SingleLiveEvent() val showSnackbarMessageEvent = SingleLiveEvent() val currentPath = MutableLiveData(EMPTY) + val showFileMenuDialog = SingleLiveEvent>() private var isPermissionRejected = false private var filePathToFileMap: Map = emptyMap() @@ -65,10 +68,13 @@ class FilePickerViewModel( init { eventProvider.subscribe(this) { event -> - if (event.containsKey(FileCellViewModel.CLICK_EVENT)) { - val filePath = event.getString(FileCellViewModel.CLICK_EVENT) - if (filePath != null) { - onItemClicked(filePath) + when { + event.containsKey(FileCellViewModel.CLICK_EVENT) -> { + onItemClicked(event.getString(FileCellViewModel.CLICK_EVENT)) + } + + event.containsKey(FileCellViewModel.LONG_CLICK_EVENT) -> { + onItemLongClicked(event.getString(FileCellViewModel.LONG_CLICK_EVENT)) } } } @@ -123,23 +129,52 @@ class FilePickerViewModel( } } - fun onDoneButtonClicked() { + fun onFileMenuClicked(item: FileMenuItem) { + val file = selectedFile ?: return + + when (item) { + FileMenuItem.SELECT -> selectFile(file) + FileMenuItem.COPY_AND_SELECT -> copyAndSelectFile(file) + } + } + + private fun selectFile(file: FileDescriptor) { if (args.action == Action.PICK_DIRECTORY) { val currentDir = currentDir ?: return router.exit() router.sendResult(FilePickerScreen.RESULT_KEY, currentDir) } else if (args.action == Action.PICK_FILE) { - if (isAnyFileSelected()) { - val selectedFile = selectedFile ?: return + router.exit() + router.sendResult(FilePickerScreen.RESULT_KEY, file) + } + } - router.exit() - router.sendResult(FilePickerScreen.RESULT_KEY, selectedFile) - } else { - showSnackbarMessageEvent.call( - resourceProvider.getString(R.string.please_select_any_file) - ) + private fun copyAndSelectFile(file: FileDescriptor) { + setScreenState(ScreenState.loading()) + + viewModelScope.launch { + val copyResult = interactor.copyToPrivateStorage(file) + if (copyResult.isFailed) { + val message = errorInteractor.processAndGetMessage(copyResult.error) + setScreenState(ScreenState.dataWithError(message)) + return@launch } + + val copiedFile = copyResult.getOrThrow() + selectFile(copiedFile) + } + } + + fun onDoneButtonClicked() { + val file = selectedFile + + if (file != null) { + selectFile(file) + } else { + showSnackbarMessageEvent.call( + resourceProvider.getString(R.string.please_select_any_file) + ) } } @@ -274,7 +309,9 @@ class FilePickerViewModel( } } - private fun onItemClicked(filePath: String) { + private fun onItemClicked(filePath: String?) { + if (filePath == null) return + val selectedFile = filePathToFileMap[filePath] ?: return if (selectedFile.isDirectory) { @@ -310,6 +347,28 @@ class FilePickerViewModel( } } + private fun onItemLongClicked(filePath: String?) { + val file = filePathToFileMap[filePath] ?: return + + selectedFile = file + + val menuItems = mutableListOf() + + if ((args.action == Action.PICK_FILE && !file.isDirectory) || + (args.action == Action.PICK_DIRECTORY && file.isDirectory) + ) { + menuItems.add(FileMenuItem.SELECT) + } + + if (!file.isDirectory && file.fsAuthority.type != FSType.INTERNAL_STORAGE) { + menuItems.add(FileMenuItem.COPY_AND_SELECT) + } + + if (menuItems.isNotEmpty()) { + showFileMenuDialog.call(menuItems) + } + } + private fun sortFiles(files: List): List { return files.sortedWith { lhs, rhs -> if ((lhs.isDirectory && !rhs.isDirectory) || (!lhs.isDirectory && rhs.isDirectory)) { @@ -352,6 +411,11 @@ class FilePickerViewModel( (currentScreenState.isDisplayingData || currentScreenState.isDisplayingEmptyState) } + enum class FileMenuItem { + SELECT, + COPY_AND_SELECT + } + class Factory(private val args: FilePickerArgs) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt index db0f2f01..6e207ad8 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/GroupsViewModel.kt @@ -13,7 +13,6 @@ import com.ivanovsky.passnotes.data.crypto.biometric.BiometricEncoder import com.ivanovsky.passnotes.data.entity.EncryptedDatabaseEntry import com.ivanovsky.passnotes.data.entity.FileDescriptor import com.ivanovsky.passnotes.data.entity.Group -import com.ivanovsky.passnotes.data.entity.KeyType import com.ivanovsky.passnotes.data.entity.Note import com.ivanovsky.passnotes.data.entity.OperationError import com.ivanovsky.passnotes.data.entity.OperationError.MESSAGE_UID_IS_NULL @@ -23,6 +22,7 @@ import com.ivanovsky.passnotes.data.entity.OperationResult import com.ivanovsky.passnotes.data.entity.Template import com.ivanovsky.passnotes.data.entity.UsedFile import com.ivanovsky.passnotes.data.repository.encdb.EncryptedDatabaseKey +import com.ivanovsky.passnotes.data.repository.keepass.FileKeepassKey import com.ivanovsky.passnotes.data.repository.keepass.PasswordKeepassKey import com.ivanovsky.passnotes.data.repository.settings.OnSettingsChangeListener import com.ivanovsky.passnotes.data.repository.settings.Settings @@ -599,7 +599,11 @@ class GroupsViewModel( return@launch } - val password = (getKeyResult.obj as PasswordKeepassKey).password + val password = when (val key = getKeyResult.getOrThrow()) { + is PasswordKeepassKey -> key.password + is FileKeepassKey -> key.password + else -> throw IllegalStateException() + } ?: EMPTY val encryptPasswordResult = interactor.encodePasswordAndStoreData( encoder, @@ -1315,8 +1319,7 @@ class GroupsViewModel( private fun isBiometricUnlockAllowedForDatabase(): Boolean { return biometricInteractor.isBiometricUnlockAvailable() && - settings.isBiometricUnlockEnabled && - dbUsedFile?.keyType == KeyType.PASSWORD + settings.isBiometricUnlockEnabled } enum class OptionPanelState { diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/dialog/ChooseOptionDialog.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/dialog/ChooseOptionDialog.kt index ec5de720..264c26ec 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/dialog/ChooseOptionDialog.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/groups/dialog/ChooseOptionDialog.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.fragment.app.DialogFragment import com.ivanovsky.passnotes.R +@Deprecated("Use OptionDialog instead") class ChooseOptionDialog : DialogFragment(), DialogInterface.OnClickListener { lateinit var onItemClickListener: (itemIndex: Int) -> Unit diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/StorageListViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/StorageListViewModel.kt index 6183336b..14c157c1 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/StorageListViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/StorageListViewModel.kt @@ -31,7 +31,7 @@ import com.ivanovsky.passnotes.presentation.core.DefaultScreenStateHandler import com.ivanovsky.passnotes.presentation.core.ScreenState import com.ivanovsky.passnotes.presentation.core.ViewModelTypes import com.ivanovsky.passnotes.presentation.core.event.SingleLiveEvent -import com.ivanovsky.passnotes.presentation.core.viewmodel.SingleTextCellViewModel +import com.ivanovsky.passnotes.presentation.core.viewmodel.OneLineTextCellViewModel import com.ivanovsky.passnotes.presentation.core.viewmodel.TwoTextWithIconCellViewModel import com.ivanovsky.passnotes.presentation.filepicker.FilePickerArgs import com.ivanovsky.passnotes.presentation.serverLogin.ServerLoginArgs @@ -54,7 +54,7 @@ class StorageListViewModel( ) : BaseScreenViewModel() { val viewTypes = ViewModelTypes() - .add(SingleTextCellViewModel::class, R.layout.cell_single_text) + .add(OneLineTextCellViewModel::class, R.layout.cell_single_text) .add(TwoTextWithIconCellViewModel::class, R.layout.cell_two_text_with_icon) val screenStateHandler = DefaultScreenStateHandler() @@ -231,8 +231,8 @@ class StorageListViewModel( private fun subscribeToEvents() { eventProvider.subscribe(this) { event -> when { - event.containsKey(SingleTextCellViewModel.CLICK_EVENT) -> { - val id = event.getString(SingleTextCellViewModel.CLICK_EVENT) ?: EMPTY + event.containsKey(OneLineTextCellViewModel.CLICK_EVENT) -> { + val id = event.getString(OneLineTextCellViewModel.CLICK_EVENT) ?: EMPTY val fsType = FSType.findByValue(id) ?: throw IllegalArgumentException() onStorageOptionClicked(fsType) } diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellModelFactory.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellModelFactory.kt index 264bb297..0c374aaf 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellModelFactory.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellModelFactory.kt @@ -5,7 +5,7 @@ import com.ivanovsky.passnotes.data.entity.FSType import com.ivanovsky.passnotes.domain.ResourceProvider import com.ivanovsky.passnotes.domain.entity.StorageOption import com.ivanovsky.passnotes.presentation.core.model.BaseCellModel -import com.ivanovsky.passnotes.presentation.core.model.SingleTextCellModel +import com.ivanovsky.passnotes.presentation.core.model.OneLineTextCellModel import com.ivanovsky.passnotes.presentation.core.model.TwoTextWithIconCellModel class StorageListCellModelFactory( @@ -31,7 +31,7 @@ class StorageListCellModelFactory( } else -> { - SingleTextCellModel( + OneLineTextCellModel( id = fsType.value, text = fsType.getTitle(isExternalStorageEnabled) ) diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellViewModelFactory.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellViewModelFactory.kt index 1cf78357..64de5a15 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellViewModelFactory.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/storagelist/factory/StorageListCellViewModelFactory.kt @@ -4,9 +4,9 @@ import com.ivanovsky.passnotes.presentation.core.BaseCellViewModel import com.ivanovsky.passnotes.presentation.core.event.EventProvider import com.ivanovsky.passnotes.presentation.core.factory.CellViewModelFactory import com.ivanovsky.passnotes.presentation.core.model.BaseCellModel -import com.ivanovsky.passnotes.presentation.core.model.SingleTextCellModel +import com.ivanovsky.passnotes.presentation.core.model.OneLineTextCellModel import com.ivanovsky.passnotes.presentation.core.model.TwoTextWithIconCellModel -import com.ivanovsky.passnotes.presentation.core.viewmodel.SingleTextCellViewModel +import com.ivanovsky.passnotes.presentation.core.viewmodel.OneLineTextCellViewModel import com.ivanovsky.passnotes.presentation.core.viewmodel.TwoTextWithIconCellViewModel class StorageListCellViewModelFactory : CellViewModelFactory { @@ -16,7 +16,7 @@ class StorageListCellViewModelFactory : CellViewModelFactory { eventProvider: EventProvider ): BaseCellViewModel { return when (model) { - is SingleTextCellModel -> SingleTextCellViewModel(model, eventProvider) + is OneLineTextCellModel -> OneLineTextCellViewModel(model, eventProvider) is TwoTextWithIconCellModel -> TwoTextWithIconCellViewModel(model, eventProvider) else -> throwUnsupportedModelException(model) } diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt index 51c9059f..768ea178 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/presentation/unlock/UnlockViewModel.kt @@ -33,6 +33,7 @@ import com.ivanovsky.passnotes.domain.interactor.unlock.UnlockInteractor import com.ivanovsky.passnotes.extensions.getFileDescriptor import com.ivanovsky.passnotes.extensions.getKeyFileDescriptor import com.ivanovsky.passnotes.extensions.getLoginType +import com.ivanovsky.passnotes.extensions.getOrThrow import com.ivanovsky.passnotes.extensions.toUsedFile import com.ivanovsky.passnotes.injection.GlobalInjector import com.ivanovsky.passnotes.presentation.ApplicationLaunchMode @@ -196,10 +197,7 @@ class UnlockViewModel( val selectedKeyFile = selectedKeyFile val password = password.value ?: EMPTY - if (isBiometricAuthenticationAvailable() && - biometricData != null && - selectedKeyFile == null - ) { + if (isBiometricAuthenticationAvailable() && biometricData != null) { val getDecoderResult = biometricInteractor.getCipherForDecryption(biometricData) if (getDecoderResult.isSucceeded) { showBiometricUnlockDialog.call(getDecoderResult.obj) @@ -383,17 +381,33 @@ class UnlockViewModel( val selectedUsedFile = selectedUsedFile ?: return val biometricData = selectedUsedFile.biometricData ?: return val selectedFile = selectedUsedFile.getFileDescriptor() + val keyFile = selectedKeyFile + + if (selectedUsedFile.keyType == KeyType.KEY_FILE && keyFile == null) return setScreenState(ScreenState.loading()) viewModelScope.launch { - val passwordResult = interactor.decodePassword(decoder, biometricData) - if (passwordResult.isFailed) { - setErrorPanelState(passwordResult.error) + val decodePasswordResult = interactor.decodePassword(decoder, biometricData) + if (decodePasswordResult.isFailed) { + setErrorPanelState(decodePasswordResult.error) return@launch } - val key = PasswordKeepassKey(passwordResult.obj) + val password = decodePasswordResult.getOrThrow() + val keyType = selectedUsedFile.keyType + + val key = when { + keyType == KeyType.PASSWORD -> PasswordKeepassKey(password) + + keyType == KeyType.KEY_FILE && keyFile != null -> FileKeepassKey( + file = keyFile, + password = password + ) + + else -> throw IllegalStateException() + } + val openResult = interactor.openDatabase(key, selectedFile) if (openResult.isFailed) { setErrorPanelState(openResult.error) @@ -882,8 +896,7 @@ class UnlockViewModel( @DrawableRes private fun getUnlockIconResIdInternal(): Int { return if (isBiometricAuthenticationAvailable() && - selectedUsedFile?.biometricData != null && - selectedKeyFile == null + selectedUsedFile?.biometricData != null ) { R.drawable.ic_fingerprint_24dp } else { diff --git a/app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt b/app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt index c3816d2c..f619986c 100644 --- a/app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt +++ b/app/src/main/kotlin/com/ivanovsky/passnotes/util/InputOutputUtils.kt @@ -39,6 +39,25 @@ object InputOutputUtils { } } + fun copy( + from: InputStream, + to: OutputStream, + isClose: Boolean + ): OperationResult { + return try { + copyOrThrow( + from = from, + to = to, + isCloseOnFinish = isClose, + cancellation = UNCANCELABLE + ) + OperationResult.success(Unit) + } catch (exception: IOException) { + Timber.d(exception) + OperationResult.error(newGenericIOError(exception)) + } + } + @JvmStatic fun copy( sourceFile: File, @@ -62,7 +81,7 @@ object InputOutputUtils { copyOrThrow( source, destination, - isCloneOnFinish = true, + isCloseOnFinish = true, cancellation = UNCANCELABLE ) OperationResult.success(Unit) @@ -88,7 +107,7 @@ object InputOutputUtils { copyOrThrow( source, destination, - isCloneOnFinish = true, + isCloseOnFinish = true, cancellation = UNCANCELABLE ) OperationResult.success(Unit) @@ -111,22 +130,22 @@ object InputOutputUtils { @JvmStatic @Throws(IOException::class) fun copyOrThrow( - source: InputStream, - destination: OutputStream, - isCloneOnFinish: Boolean, + from: InputStream, + to: OutputStream, + isCloseOnFinish: Boolean, cancellation: AtomicBoolean ) { try { val buf = ByteArray(BUFFER_SIZE) var len: Int - while (source.read(buf).also { len = it } > 0 && !cancellation.get()) { - destination.write(buf, 0, len) + while (from.read(buf).also { len = it } > 0 && !cancellation.get()) { + to.write(buf, 0, len) } - destination.flush() + to.flush() } finally { - if (isCloneOnFinish) { - closeOrThrow(source) - closeOrThrow(destination) + if (isCloseOnFinish) { + closeOrThrow(from) + closeOrThrow(to) } } } diff --git a/app/src/main/res/layout/cell_file.xml b/app/src/main/res/layout/cell_file.xml index bbd90925..0f01e7ee 100644 --- a/app/src/main/res/layout/cell_file.xml +++ b/app/src/main/res/layout/cell_file.xml @@ -18,7 +18,8 @@ android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" - android:onClick="@{() -> viewModel.onClicked()}"> + android:onClick="@{() -> viewModel.onClicked()}" + app:onLongClick="@{() -> viewModel.onLongClicked()}"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cell_option_two_line.xml b/app/src/main/res/layout/cell_option_two_line.xml new file mode 100644 index 00000000..230130aa --- /dev/null +++ b/app/src/main/res/layout/cell_option_two_line.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cell_single_text.xml b/app/src/main/res/layout/cell_single_text.xml index 127bcbeb..9954e137 100644 --- a/app/src/main/res/layout/cell_single_text.xml +++ b/app/src/main/res/layout/cell_single_text.xml @@ -7,7 +7,7 @@ + type="com.ivanovsky.passnotes.presentation.core.viewmodel.OneLineTextCellViewModel" /> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b229df16..ed60771a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -290,6 +290,8 @@ Grant permission To complete this action application needs the permission to manage all files. After pressing "Grant" button, please select Allow access to manage all files option on the next screen Unable to authenticate + Make a copy and Select + Copy will be created inside application private directory Login to server