Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import eu.opencloud.android.data.authentication.datasources.LocalAuthenticationD
import eu.opencloud.android.data.authentication.datasources.implementation.OCLocalAuthenticationDataSource
import eu.opencloud.android.data.capabilities.datasources.LocalCapabilitiesDataSource
import eu.opencloud.android.data.capabilities.datasources.implementation.OCLocalCapabilitiesDataSource
import eu.opencloud.android.data.exportjobs.datasources.LocalExportJobDataSource
import eu.opencloud.android.data.exportjobs.datasources.implementation.OCLocalExportJobDataSource
import eu.opencloud.android.data.files.datasources.LocalFileDataSource
import eu.opencloud.android.data.files.datasources.implementation.OCLocalFileDataSource
import eu.opencloud.android.data.folderbackup.datasources.LocalFolderBackupDataSource
Expand Down Expand Up @@ -59,6 +61,7 @@ val localDataSourceModule = module {

single { OpencloudDatabase.getDatabase(androidContext()).appRegistryDao() }
single { OpencloudDatabase.getDatabase(androidContext()).capabilityDao() }
single { OpencloudDatabase.getDatabase(androidContext()).exportJobDao() }
single { OpencloudDatabase.getDatabase(androidContext()).fileDao() }
single { OpencloudDatabase.getDatabase(androidContext()).folderBackUpDao() }
single { OpencloudDatabase.getDatabase(androidContext()).shareDao() }
Expand All @@ -73,6 +76,7 @@ val localDataSourceModule = module {
factoryOf(::OCLocalFolderBackupDataSource) bind LocalFolderBackupDataSource::class
factoryOf(::OCLocalAppRegistryDataSource) bind LocalAppRegistryDataSource::class
factoryOf(::OCLocalCapabilitiesDataSource) bind LocalCapabilitiesDataSource::class
factoryOf(::OCLocalExportJobDataSource) bind LocalExportJobDataSource::class
factoryOf(::OCLocalFileDataSource) bind LocalFileDataSource::class
factoryOf(::OCLocalShareDataSource) bind LocalShareDataSource::class
factoryOf(::OCLocalSpacesDataSource) bind LocalSpacesDataSource::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ package eu.opencloud.android.dependecyinjection
import eu.opencloud.android.data.appregistry.repository.OCAppRegistryRepository
import eu.opencloud.android.data.authentication.repository.OCAuthenticationRepository
import eu.opencloud.android.data.capabilities.repository.OCCapabilityRepository
import eu.opencloud.android.data.exportjobs.repository.OCExportJobRepository
import eu.opencloud.android.data.files.repository.OCFileRepository
import eu.opencloud.android.data.folderbackup.repository.OCFolderBackupRepository
import eu.opencloud.android.data.oauth.repository.OCOAuthRepository
Expand All @@ -40,6 +41,7 @@ import eu.opencloud.android.domain.authentication.AuthenticationRepository
import eu.opencloud.android.domain.authentication.oauth.OAuthRepository
import eu.opencloud.android.domain.automaticuploads.FolderBackupRepository
import eu.opencloud.android.domain.capabilities.CapabilityRepository
import eu.opencloud.android.domain.exportjobs.ExportJobRepository
import eu.opencloud.android.domain.files.FileRepository
import eu.opencloud.android.domain.server.ServerInfoRepository
import eu.opencloud.android.domain.sharing.sharees.ShareeRepository
Expand All @@ -56,6 +58,7 @@ val repositoryModule = module {
factoryOf(::OCAppRegistryRepository) bind AppRegistryRepository::class
factoryOf(::OCAuthenticationRepository) bind AuthenticationRepository::class
factoryOf(::OCCapabilityRepository) bind CapabilityRepository::class
factoryOf(::OCExportJobRepository) bind ExportJobRepository::class
factoryOf(::OCFileRepository) bind FileRepository::class
factoryOf(::OCFolderBackupRepository) bind FolderBackupRepository::class
factoryOf(::OCOAuthRepository) bind OAuthRepository::class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import eu.opencloud.android.domain.files.usecases.GetFolderContentAsStreamUseCas
import eu.opencloud.android.domain.files.usecases.GetFolderContentUseCase
import eu.opencloud.android.domain.files.usecases.GetFolderImagesUseCase
import eu.opencloud.android.domain.files.usecases.GetPersonalRootFolderForAccountUseCase
import eu.opencloud.android.domain.files.usecases.GetRemoteFolderContentUseCase
import eu.opencloud.android.domain.files.usecases.GetSearchFolderContentUseCase
import eu.opencloud.android.domain.files.usecases.GetSharedByLinkForAccountAsStreamUseCase
import eu.opencloud.android.domain.files.usecases.GetSharesRootFolderForAccount
Expand Down Expand Up @@ -112,6 +113,7 @@ import eu.opencloud.android.domain.user.usecases.RefreshUserQuotaFromServerAsync
import eu.opencloud.android.domain.webfinger.usecases.GetOpenCloudInstanceFromWebFingerUseCase
import eu.opencloud.android.domain.webfinger.usecases.GetOpenCloudInstancesFromAuthenticatedWebFingerUseCase
import eu.opencloud.android.usecases.accounts.RemoveAccountUseCase
import eu.opencloud.android.usecases.files.ExportFilesToDeviceUseCase
import eu.opencloud.android.usecases.files.FilterFileMenuOptionsUseCase
import eu.opencloud.android.usecases.files.RemoveLocalFilesForAccountUseCase
import eu.opencloud.android.usecases.files.RemoveLocallyFilesWithLastUsageOlderThanGivenTimeUseCase
Expand Down Expand Up @@ -165,6 +167,7 @@ val useCaseModule = module {
factoryOf(::CreateFolderAsyncUseCase)
factoryOf(::DisableThumbnailsForFileUseCase)
factoryOf(::FilterFileMenuOptionsUseCase)
factoryOf(::ExportFilesToDeviceUseCase)
factoryOf(::GetFileByIdAsStreamUseCase)
factoryOf(::GetFileByIdUseCase)
factoryOf(::GetFileByRemotePathUseCase)
Expand All @@ -174,6 +177,7 @@ val useCaseModule = module {
factoryOf(::GetFolderImagesUseCase)
factoryOf(::IsAnyFileAvailableLocallyAndNotAvailableOfflineUseCase)
factoryOf(::GetPersonalRootFolderForAccountUseCase)
factoryOf(::GetRemoteFolderContentUseCase)
factoryOf(::GetSearchFolderContentUseCase)
factoryOf(::GetSharedByLinkForAccountAsStreamUseCase)
factoryOf(::GetSharesRootFolderForAccount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ val viewModelModule = module {
ShareViewModel(filePath, accountName, get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
viewModel { (initialFolderToDisplay: OCFile, fileListOption: FileListOption) ->
MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
MainFileListViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
initialFolderToDisplay, fileListOption)
}
viewModel { (ocFile: OCFile) -> ConflictsResolveViewModel(get(), get(), get(), get(), get(), ocFile) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fun FileMenuOption.toResId() =
FileMenuOption.SHARE -> R.id.action_share_file
FileMenuOption.DETAILS -> R.id.action_see_details
FileMenuOption.SEND -> R.id.action_send_file
FileMenuOption.EXPORT -> R.id.action_export_file
FileMenuOption.SET_AV_OFFLINE -> R.id.action_set_available_offline
FileMenuOption.UNSET_AV_OFFLINE -> R.id.action_unset_available_offline
}
Expand All @@ -57,6 +58,7 @@ fun FileMenuOption.toStringResId() =
FileMenuOption.SHARE -> R.string.action_share
FileMenuOption.DETAILS -> R.string.actionbar_see_details
FileMenuOption.SEND -> R.string.actionbar_send_file
FileMenuOption.EXPORT -> R.string.actionbar_export_file
FileMenuOption.SET_AV_OFFLINE -> R.string.set_available_offline
FileMenuOption.UNSET_AV_OFFLINE -> R.string.unset_available_offline
}
Expand All @@ -76,6 +78,7 @@ fun FileMenuOption.toDrawableResId() =
FileMenuOption.SHARE -> R.drawable.ic_share_generic_white
FileMenuOption.DETAILS -> R.drawable.ic_info_white
FileMenuOption.SEND -> R.drawable.ic_send_white
FileMenuOption.EXPORT -> R.drawable.ic_action_save_to_device
FileMenuOption.SET_AV_OFFLINE -> R.drawable.ic_action_set_available_offline
FileMenuOption.UNSET_AV_OFFLINE -> R.drawable.ic_action_unset_available_offline
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ class FileDetailsViewModel(
shareViaLinkAllowed = shareViaLinkAllowed,
shareWithUsersAllowed = shareWithUsersAllowed,
sendAllowed = sendAllowed,
// Exporting to a device folder is offered from the file list (single and
// multi-select), not from the details screen.
exportAllowed = false,
)
)
result.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import android.view.WindowManager
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
Expand All @@ -54,6 +55,7 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import coil.load
Expand Down Expand Up @@ -122,6 +124,7 @@ import eu.opencloud.android.utils.DisplayUtils
import eu.opencloud.android.utils.MimetypeIconUtil
import eu.opencloud.android.utils.PreferenceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.Path.Companion.toPath
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
Expand Down Expand Up @@ -183,6 +186,37 @@ class MainFileListFragment : Fragment(),

private var menu: Menu? = null
private var checkedFiles: List<OCFile> = emptyList()

// The unbounded selection itself lives in Room before the external picker is opened. Only its
// fixed-size job id is kept in Fragment state while the picker outlives this process.
private var pendingExportJobId: Long? = null
private var isPreparingExport = false

private val exportToDeviceFolderLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { treeUri ->
val exportJobId = pendingExportJobId
clearPendingExport()
if (exportJobId != null) {
if (treeUri == null) {
mainFileListViewModel.discardPendingExport(exportJobId)
} else {
val permissionTaken = runCatching {
requireContext().contentResolver.takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}.onFailure {
Timber.e(it, "Could not retain access to the selected export folder")
}.isSuccess

if (permissionTaken) {
mainFileListViewModel.exportFilesToDevice(exportJobId, treeUri.toString())
} else {
mainFileListViewModel.discardPendingExport(exportJobId)
}
}
}
}
private var filesToRemove: List<OCFile> = emptyList()
private var fileSingleFile: OCFile? = null
private var fileOptionsBottomSheetSingleFileLayout: LinearLayout? = null
Expand Down Expand Up @@ -316,6 +350,27 @@ class MainFileListFragment : Fragment(),
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let { savedState ->
pendingExportJobId = if (savedState.containsKey(KEY_PENDING_EXPORT_JOB_ID)) {
savedState.getLong(KEY_PENDING_EXPORT_JOB_ID)
} else {
null
}
// The picker outlives this process, so the restored selection is claimed again before
// anything else of this account can collect it.
pendingExportJobId?.let { mainFileListViewModel.retainPendingExport(it) }
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// The SAF folder picker is another app, this fragment may be recreated while it is shown.
pendingExportJobId?.let { outState.putLong(KEY_PENDING_EXPORT_JOB_ID, it) }
?: outState.remove(KEY_PENDING_EXPORT_JOB_ID)
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
Expand Down Expand Up @@ -344,6 +399,42 @@ class MainFileListFragment : Fragment(),
}
}

/**
* Persists what has to be exported and only then asks the user for the destination folder.
* The selection is consumed once the picker returns, see [exportToDeviceFolderLauncher].
*/
private fun startExportToDeviceFolder(files: List<OCFile>) {
val accountName = files.firstOrNull()?.owner
val fileIds = files.mapNotNull { it.id }
if (accountName == null || fileIds.isEmpty()) {
Timber.e("Nothing that could be exported was selected")
return
}
if (pendingExportJobId != null || isPreparingExport) {
Timber.w("An export selection is already waiting for a device folder")
return
}

isPreparingExport = true
viewLifecycleOwner.lifecycleScope.launch {
val exportJobId = try {
mainFileListViewModel.prepareExportToDevice(fileIds, accountName)
} finally {
isPreparingExport = false
}
if (exportJobId == null) {
Timber.e("The export selection could not be persisted")
} else {
pendingExportJobId = exportJobId
exportToDeviceFolderLauncher.launch(null)
}
}
}

private fun clearPendingExport() {
pendingExportJobId = null
}

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
(menu.findItem(R.id.action_search).actionView as SearchView).run {
Expand Down Expand Up @@ -718,6 +809,10 @@ class MainFileListFragment : Fragment(),
}
}

FileMenuOption.EXPORT -> {
startExportToDeviceFolder(listOf(file))
}

FileMenuOption.SET_AV_OFFLINE -> {
fileOperationsViewModel.performOperation(FileOperation.SetFilesAsAvailableOffline(listOf(file)))
if (file.isFolder) {
Expand Down Expand Up @@ -1476,6 +1571,11 @@ class MainFileListFragment : Fragment(),
true
}

R.id.action_export_file -> {
startExportToDeviceFolder(checkedFiles)
true
}

R.id.action_move -> {
val action = Intent(activity, FolderPickerActivity::class.java)
action.putParcelableArrayListExtra(FolderPickerActivity.EXTRA_FILES, checkedFiles)
Expand Down Expand Up @@ -1619,6 +1719,8 @@ class MainFileListFragment : Fragment(),

private const val FILE_DOCXF_EXTENSION = "docxf"

private const val KEY_PENDING_EXPORT_JOB_ID = "KEY_PENDING_EXPORT_JOB_ID"

@JvmStatic
fun newInstance(
initialFolderToDisplay: OCFile,
Expand Down
Loading