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 @@ -112,6 +112,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 +166,7 @@ val useCaseModule = module {
factoryOf(::CreateFolderAsyncUseCase)
factoryOf(::DisableThumbnailsForFileUseCase)
factoryOf(::FilterFileMenuOptionsUseCase)
factoryOf(::ExportFilesToDeviceUseCase)
factoryOf(::GetFileByIdAsStreamUseCase)
factoryOf(::GetFileByIdUseCase)
factoryOf(::GetFileByRemotePathUseCase)
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.contracts.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
Expand Down Expand Up @@ -183,6 +184,26 @@ class MainFileListFragment : Fragment(),

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

// Files/folders the user chose to export; consumed once the SAF folder picker returns. The
// external picker can outlive this fragment, and even this process, so the selection is kept
// in the saved instance state and only cleared once the result has been handled.
private var pendingExportFileIds: List<Long> = emptyList()
private var pendingExportAccountName: String? = null

private val exportToDeviceFolderLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { treeUri ->
val fileIdsToExport = pendingExportFileIds
val accountName = pendingExportAccountName
clearPendingExport()
if (treeUri != null && fileIdsToExport.isNotEmpty() && accountName != null) {
requireContext().contentResolver.takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
mainFileListViewModel.exportFilesToDevice(fileIdsToExport, accountName, treeUri.toString())
}
}
private var filesToRemove: List<OCFile> = emptyList()
private var fileSingleFile: OCFile? = null
private var fileOptionsBottomSheetSingleFileLayout: LinearLayout? = null
Expand Down Expand Up @@ -316,6 +337,21 @@ class MainFileListFragment : Fragment(),
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let { savedState ->
pendingExportFileIds = savedState.getLongArray(KEY_PENDING_EXPORT_FILE_IDS)?.toList().orEmpty()
pendingExportAccountName = savedState.getString(KEY_PENDING_EXPORT_ACCOUNT_NAME)
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// The SAF folder picker is another app, this fragment may be recreated while it is shown.
outState.putLongArray(KEY_PENDING_EXPORT_FILE_IDS, pendingExportFileIds.toLongArray())
outState.putString(KEY_PENDING_EXPORT_ACCOUNT_NAME, pendingExportAccountName)
}

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

/**
* Remembers what has to be exported and asks the user for the destination folder. The
* selection is only 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
}
pendingExportFileIds = fileIds
pendingExportAccountName = accountName
exportToDeviceFolderLauncher.launch(null)
}

private fun clearPendingExport() {
pendingExportFileIds = emptyList()
pendingExportAccountName = 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 +775,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 +1537,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 +1685,9 @@ class MainFileListFragment : Fragment(),

private const val FILE_DOCXF_EXTENSION = "docxf"

private const val KEY_PENDING_EXPORT_FILE_IDS = "KEY_PENDING_EXPORT_FILE_IDS"
private const val KEY_PENDING_EXPORT_ACCOUNT_NAME = "KEY_PENDING_EXPORT_ACCOUNT_NAME"

@JvmStatic
fun newInstance(
initialFolderToDisplay: OCFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ import eu.opencloud.android.presentation.files.SortType.Companion.PREF_FILE_LIST
import eu.opencloud.android.presentation.settings.advanced.SettingsAdvancedFragment.Companion.PREF_SHOW_HIDDEN_FILES
import eu.opencloud.android.providers.ContextProvider
import eu.opencloud.android.providers.CoroutinesDispatcherProvider
import eu.opencloud.android.usecases.files.ExportFilesToDeviceUseCase
import eu.opencloud.android.usecases.files.FilterFileMenuOptionsUseCase
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase.SyncFolderMode.SYNC_CONTENTS
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -90,12 +92,34 @@ class MainFileListViewModel(
private val contextProvider: ContextProvider,
private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider,
private val sharedPreferencesProvider: SharedPreferencesProvider,
private val exportFilesToDeviceUseCase: ExportFilesToDeviceUseCase,
initialFolderToDisplay: OCFile,
fileListOptionParam: FileListOption,
) : ViewModel() {

private val showHiddenFiles: Boolean = sharedPreferencesProvider.getBoolean(PREF_SHOW_HIDDEN_FILES, false)

/**
* Enqueues a background export of the given files/folders into the device folder the user
* picked through the Storage Access Framework. See opencloud-eu/android#180.
*
* The selection is persisted before the worker is enqueued, so this runs off the main thread.
* It does not run on the ViewModel scope either: the picker returns to an activity that may
* already be finishing, and an export the user asked for must not be dropped then.
*/
fun exportFilesToDevice(fileIds: List<Long>, accountName: String, targetFolderTreeUri: String) {
if (fileIds.isEmpty()) return
CoroutineScope(coroutinesDispatcherProvider.io).launch {
exportFilesToDeviceUseCase(
ExportFilesToDeviceUseCase.Params(
accountName = accountName,
fileIds = fileIds,
targetFolderTreeUri = targetFolderTreeUri,
)
)
}
}

val currentFolderDisplayed: MutableStateFlow<OCFile> = MutableStateFlow(initialFolderToDisplay)
val fileListOption: MutableStateFlow<FileListOption> = MutableStateFlow(fileListOptionParam)
private val searchFilter: MutableStateFlow<String> = MutableStateFlow("")
Expand Down Expand Up @@ -324,6 +348,8 @@ class MainFileListViewModel(
shareViaLinkAllowed = shareViaLinkAllowed,
shareWithUsersAllowed = shareWithUsersAllowed,
sendAllowed = sendAllowed,
// The file list is the only screen that handles the export action.
exportAllowed = true,
)
)
if (isMultiselection) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class PreviewAudioViewModel(
shareViaLinkAllowed = shareViaLinkAllowed,
shareWithUsersAllowed = shareWithUsersAllowed,
sendAllowed = sendAllowed,
// This screen has no handler for the export action.
exportAllowed = false,
)
)
result.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class PreviewTextViewModel(
shareViaLinkAllowed = shareViaLinkAllowed,
shareWithUsersAllowed = shareWithUsersAllowed,
sendAllowed = sendAllowed,
// This screen has no handler for the export action.
exportAllowed = false,
)
)
result.apply {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class PreviewVideoViewModel(
shareViaLinkAllowed = shareViaLinkAllowed,
shareWithUsersAllowed = shareWithUsersAllowed,
sendAllowed = sendAllowed,
// This screen has no handler for the export action.
exportAllowed = false,
)
)
_menuOptions.update { result }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class PreviewImageViewModel(
shareViaLinkAllowed = shareViaLinkAllowed,
shareWithUsersAllowed = shareWithUsersAllowed,
sendAllowed = sendAllowed,
// This screen has no handler for the export action.
exportAllowed = false,
)
)
result.apply {
Expand Down
Loading