diff --git a/owncloud-android-library b/owncloud-android-library index 8a89edb5bea..21969fa98c7 160000 --- a/owncloud-android-library +++ b/owncloud-android-library @@ -1 +1 @@ -Subproject commit 8a89edb5bea43abc1228323ef74f99556153f20c +Subproject commit 21969fa98c72b1f9fa3336ffa2231aa018ba4961 diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java b/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java index a30646e4352..848cda72494 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java +++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java @@ -39,6 +39,7 @@ import com.owncloud.android.R; import com.owncloud.android.domain.files.model.OCFile; import com.owncloud.android.domain.files.usecases.DisableThumbnailsForFileUseCase; +import com.owncloud.android.domain.spaces.model.SpaceSpecial; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.SingleSessionManager; @@ -75,6 +76,7 @@ public class ThumbnailsCacheManager { private static OwnCloudClient mClient = null; private static final String PREVIEW_URI = "%s/remote.php/dav/files/%s%s?x=%d&y=%d&c=%s&preview=1"; + private static final String SPACE_SPECIAL_URI = "%s?scalingup=0&a=1&x=%d&y=%d&c=%s&preview=1"; public static Bitmap mDefaultImg = BitmapFactory.decodeResource( @@ -186,6 +188,8 @@ protected Bitmap doInBackground(Object... params) { thumbnail = doOCFileInBackground(); } else if (mFile instanceof File) { thumbnail = doFileInBackground(); + } else if (mFile instanceof SpaceSpecial) { + thumbnail = doSpaceImageInBackground(); //} else { do nothing } @@ -210,6 +214,8 @@ protected void onPostExecute(Bitmap bitmap) { tagId = String.valueOf(((OCFile) mFile).getId()); } else if (mFile instanceof File) { tagId = String.valueOf(mFile.hashCode()); + } else if (mFile instanceof SpaceSpecial) { + tagId = ((SpaceSpecial) mFile).getId(); } if (String.valueOf(imageView.getTag()).equals(tagId)) { imageView.setImageBitmap(bitmap); @@ -349,6 +355,61 @@ private Bitmap doFileInBackground() { return thumbnail; } + private String getSpaceSpecialUri(SpaceSpecial spaceSpecial) { + return String.format(Locale.ROOT, + SPACE_SPECIAL_URI, + spaceSpecial.getWebDavUrl(), + getThumbnailDimension(), + getThumbnailDimension(), + spaceSpecial.getETag()); + } + + private Bitmap doSpaceImageInBackground() { + SpaceSpecial spaceSpecial = (SpaceSpecial) mFile; + + final String imageKey = spaceSpecial.getId(); + + // Check disk cache in background thread + Bitmap thumbnail = getBitmapFromDiskCache(imageKey); + + // Not found in disk cache + if (thumbnail == null) { // TODO: Check if the current thumbnail is outdated + int px = getThumbnailDimension(); + + // Download thumbnail from server + if (mClient != null) { + GetMethod get; + try { + String uri = getSpaceSpecialUri(spaceSpecial); + Timber.d("URI: %s", uri); + get = new GetMethod(new URL(uri)); + int status = mClient.executeHttpMethod(get); + if (status == HttpConstants.HTTP_OK) { + InputStream inputStream = get.getResponseBodyAsStream(); + Bitmap bitmap = BitmapFactory.decodeStream(inputStream); + thumbnail = ThumbnailUtils.extractThumbnail(bitmap, px, px); + + // Handle PNG + if (spaceSpecial.getFile().getMimeType().equalsIgnoreCase("image/png")) { + thumbnail = handlePNG(thumbnail, px); + } + + // Add thumbnail to cache + if (thumbnail != null) { + addBitmapToCache(imageKey, thumbnail); + } + } else { + mClient.exhaustResponse(get.getResponseBodyAsStream()); + } + } catch (Exception e) { + Timber.e(e); + } + } + } + + return thumbnail; + + } } public static boolean cancelPotentialThumbnailWork(Object file, ImageView imageView) { diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt index 8654a6a49d0..dabbc59ea40 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/LocalDataSourceModule.kt @@ -38,6 +38,8 @@ import com.owncloud.android.data.preferences.datasources.SharedPreferencesProvid import com.owncloud.android.data.preferences.datasources.implementation.OCSharedPreferencesProvider import com.owncloud.android.data.sharing.shares.datasources.LocalShareDataSource import com.owncloud.android.data.sharing.shares.datasources.implementation.OCLocalShareDataSource +import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource +import com.owncloud.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource import com.owncloud.android.data.storage.LocalStorageProvider import com.owncloud.android.data.storage.ScopedStorageProvider import com.owncloud.android.data.transfers.datasources.LocalTransferDataSource @@ -56,6 +58,7 @@ val localDataSourceModule = module { single { OwncloudDatabase.getDatabase(androidContext()).userDao() } single { OwncloudDatabase.getDatabase(androidContext()).folderBackUpDao() } single { OwncloudDatabase.getDatabase(androidContext()).transferDao() } + single { OwncloudDatabase.getDatabase(androidContext()).spacesDao() } single { OCSharedPreferencesProvider(get()) } single { ScopedStorageProvider(dataFolder, androidContext()) } @@ -67,4 +70,5 @@ val localDataSourceModule = module { factory { OCLocalUserDataSource(get()) } factory { OCFolderBackupLocalDataSource(get()) } factory { OCLocalTransferDataSource(get()) } + factory { OCLocalSpacesDataSource(get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt index 00ef7477197..80337e63481 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RemoteDataSourceModule.kt @@ -21,7 +21,6 @@ package com.owncloud.android.dependecyinjection import com.owncloud.android.MainApp import com.owncloud.android.R -import com.owncloud.android.presentation.authentication.AccountUtils import com.owncloud.android.data.ClientManager import com.owncloud.android.data.authentication.datasources.RemoteAuthenticationDataSource import com.owncloud.android.data.authentication.datasources.implementation.OCRemoteAuthenticationDataSource @@ -40,6 +39,8 @@ import com.owncloud.android.data.sharing.sharees.datasources.mapper.RemoteSharee import com.owncloud.android.data.sharing.shares.datasources.RemoteShareDataSource import com.owncloud.android.data.sharing.shares.datasources.implementation.OCRemoteShareDataSource import com.owncloud.android.data.sharing.shares.datasources.mapper.RemoteShareMapper +import com.owncloud.android.data.spaces.datasources.RemoteSpacesDataSource +import com.owncloud.android.data.spaces.datasources.implementation.OCRemoteSpacesDataSource import com.owncloud.android.data.user.datasources.RemoteUserDataSource import com.owncloud.android.data.user.datasources.implementation.OCRemoteUserDataSource import com.owncloud.android.data.webfinger.datasources.WebfingerRemoteDatasource @@ -57,12 +58,15 @@ import com.owncloud.android.lib.resources.shares.services.ShareService import com.owncloud.android.lib.resources.shares.services.ShareeService import com.owncloud.android.lib.resources.shares.services.implementation.OCShareService import com.owncloud.android.lib.resources.shares.services.implementation.OCShareeService +import com.owncloud.android.lib.resources.spaces.services.OCSpacesService +import com.owncloud.android.lib.resources.spaces.services.SpacesService import com.owncloud.android.lib.resources.status.services.CapabilityService import com.owncloud.android.lib.resources.status.services.ServerInfoService import com.owncloud.android.lib.resources.status.services.implementation.OCCapabilityService import com.owncloud.android.lib.resources.status.services.implementation.OCServerInfoService import com.owncloud.android.lib.resources.webfinger.services.WebfingerService import com.owncloud.android.lib.resources.webfinger.services.implementation.OCWebfingerService +import com.owncloud.android.presentation.authentication.AccountUtils import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -81,6 +85,7 @@ val remoteDataSourceModule = module { single { OCOIDCService() } single { OCShareService(get()) } single { OCShareeService(get()) } + single { OCSpacesService(get()) } single { OCWebfingerService() } factory { OCRemoteAuthenticationDataSource(get()) } @@ -90,6 +95,7 @@ val remoteDataSourceModule = module { factory { OCRemoteServerInfoDataSource(get(), get()) } factory { OCRemoteShareDataSource(get(), get()) } factory { OCRemoteShareeDataSource(get(), get()) } + factory { OCRemoteSpacesDataSource(get()) } factory { OCRemoteUserDataSource(get(), androidContext().resources.getDimension(R.dimen.file_avatar_size).toInt()) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt index 73f251b80cd..96f7e630718 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/RepositoryModule.kt @@ -30,6 +30,7 @@ import com.owncloud.android.data.oauth.repository.OCOAuthRepository import com.owncloud.android.data.server.repository.OCServerInfoRepository import com.owncloud.android.data.sharing.sharees.repository.OCShareeRepository import com.owncloud.android.data.sharing.shares.repository.OCShareRepository +import com.owncloud.android.data.spaces.repository.OCSpacesRepository import com.owncloud.android.data.transfers.repository.OCTransferRepository import com.owncloud.android.data.user.repository.OCUserRepository import com.owncloud.android.data.webfinger.repository.OCWebfingerRepository @@ -41,6 +42,7 @@ import com.owncloud.android.domain.files.FileRepository import com.owncloud.android.domain.server.ServerInfoRepository import com.owncloud.android.domain.sharing.sharees.ShareeRepository import com.owncloud.android.domain.sharing.shares.ShareRepository +import com.owncloud.android.domain.spaces.SpacesRepository import com.owncloud.android.domain.transfers.TransferRepository import com.owncloud.android.domain.user.UserRepository import com.owncloud.android.domain.webfinger.WebfingerRepository @@ -53,6 +55,7 @@ val repositoryModule = module { factory { OCServerInfoRepository(get()) } factory { OCShareRepository(get(), get()) } factory { OCShareeRepository(get()) } + factory { OCSpacesRepository(get(), get()) } factory { OCUserRepository(get(), get()) } factory { OCOAuthRepository(get()) } factory { OCFolderBackupRepository(get()) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 6caf2ef91da..764aa1982ce 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -77,6 +77,8 @@ import com.owncloud.android.domain.sharing.shares.usecases.EditPublicShareAsyncU import com.owncloud.android.domain.sharing.shares.usecases.GetShareAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.GetSharesAsLiveDataUseCase import com.owncloud.android.domain.sharing.shares.usecases.RefreshSharesFromServerAsyncUseCase +import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase +import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase import com.owncloud.android.domain.transfers.usecases.ClearSuccessfulTransfersUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersAsLiveDataUseCase import com.owncloud.android.domain.transfers.usecases.GetAllTransfersUseCase @@ -91,10 +93,10 @@ import com.owncloud.android.usecases.accounts.RemoveAccountUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFileUseCase import com.owncloud.android.usecases.synchronization.SynchronizeFolderUseCase import com.owncloud.android.usecases.transfers.downloads.CancelDownloadForFileUseCase +import com.owncloud.android.usecases.transfers.downloads.CancelDownloadsRecursivelyUseCase import com.owncloud.android.usecases.transfers.downloads.DownloadFileUseCase import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForDownloadingFileUseCase import com.owncloud.android.usecases.transfers.downloads.GetLiveDataForFinishedDownloadsFromAccountUseCase -import com.owncloud.android.usecases.transfers.downloads.CancelDownloadsRecursivelyUseCase import com.owncloud.android.usecases.transfers.uploads.CancelTransfersFromAccountUseCase import com.owncloud.android.usecases.transfers.uploads.CancelUploadForFileUseCase import com.owncloud.android.usecases.transfers.uploads.CancelUploadUseCase @@ -172,6 +174,10 @@ val useCaseModule = module { factory { GetSharesAsLiveDataUseCase(get()) } factory { RefreshSharesFromServerAsyncUseCase(get()) } + // Spaces + factory { RefreshSpacesFromServerAsyncUseCase(get()) } + factory { GetProjectSpacesWithSpecialsForAccountAsStreamUseCase(get()) } + // Transfers factory { CancelDownloadForFileUseCase(get()) } factory { CancelDownloadsRecursivelyUseCase(get(), get()) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index 4c6fef9ab4f..2ced99e2cdd 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -26,33 +26,34 @@ package com.owncloud.android.dependecyinjection import com.owncloud.android.MainApp import com.owncloud.android.domain.files.model.FileListOption import com.owncloud.android.domain.files.model.OCFile -import com.owncloud.android.presentation.files.details.FileDetailsViewModel -import com.owncloud.android.presentation.files.filelist.MainFileListViewModel -import com.owncloud.android.presentation.files.operations.FileOperationsViewModel -import com.owncloud.android.presentation.security.passcode.PasscodeAction +import com.owncloud.android.presentation.accounts.AccountsManagementViewModel +import com.owncloud.android.presentation.accounts.RemoveAccountDialogViewModel import com.owncloud.android.presentation.authentication.AuthenticationViewModel +import com.owncloud.android.presentation.authentication.oauth.OAuthViewModel import com.owncloud.android.presentation.capabilities.CapabilityViewModel -import com.owncloud.android.presentation.conflicts.ConflictsResolveViewModel import com.owncloud.android.presentation.common.DrawerViewModel +import com.owncloud.android.presentation.conflicts.ConflictsResolveViewModel +import com.owncloud.android.presentation.files.details.FileDetailsViewModel +import com.owncloud.android.presentation.files.filelist.MainFileListViewModel +import com.owncloud.android.presentation.files.operations.FileOperationsViewModel import com.owncloud.android.presentation.logging.LogListViewModel import com.owncloud.android.presentation.migration.MigrationViewModel -import com.owncloud.android.presentation.authentication.oauth.OAuthViewModel import com.owncloud.android.presentation.releasenotes.ReleaseNotesViewModel import com.owncloud.android.presentation.security.biometric.BiometricViewModel import com.owncloud.android.presentation.security.passcode.PassCodeViewModel +import com.owncloud.android.presentation.security.passcode.PasscodeAction import com.owncloud.android.presentation.security.pattern.PatternViewModel +import com.owncloud.android.presentation.settings.SettingsViewModel import com.owncloud.android.presentation.settings.advanced.SettingsAdvancedViewModel +import com.owncloud.android.presentation.settings.autouploads.SettingsPictureUploadsViewModel +import com.owncloud.android.presentation.settings.autouploads.SettingsVideoUploadsViewModel import com.owncloud.android.presentation.settings.logging.SettingsLogsViewModel import com.owncloud.android.presentation.settings.more.SettingsMoreViewModel -import com.owncloud.android.presentation.settings.autouploads.SettingsPictureUploadsViewModel import com.owncloud.android.presentation.settings.security.SettingsSecurityViewModel -import com.owncloud.android.presentation.settings.autouploads.SettingsVideoUploadsViewModel -import com.owncloud.android.presentation.settings.SettingsViewModel import com.owncloud.android.presentation.sharing.ShareViewModel +import com.owncloud.android.presentation.spaces.SpacesListViewModel import com.owncloud.android.presentation.transfers.TransfersViewModel -import com.owncloud.android.presentation.accounts.AccountsManagementViewModel import com.owncloud.android.ui.ReceiveExternalFilesViewModel -import com.owncloud.android.presentation.accounts.RemoveAccountDialogViewModel import com.owncloud.android.ui.preview.PreviewImageViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -112,4 +113,5 @@ val viewModelModule = module { viewModel { (ocFile: OCFile) -> ConflictsResolveViewModel(get(), get(), get(), get(), get(), ocFile) } viewModel { ReceiveExternalFilesViewModel(get(), get()) } viewModel { AccountsManagementViewModel(get()) } + viewModel { SpacesListViewModel(get(), get(), get(), get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt new file mode 100644 index 00000000000..c9ab83bbeba --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListAdapter.kt @@ -0,0 +1,101 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.spaces + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageView +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.owncloud.android.R +import com.owncloud.android.databinding.SpacesListItemBinding +import com.owncloud.android.datamodel.ThumbnailsCacheManager +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.presentation.authentication.AccountUtils +import com.owncloud.android.utils.PreferenceUtils + +class SpacesListAdapter : RecyclerView.Adapter() { + + private val spacesList = mutableListOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val binding = SpacesListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + binding.root.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(parent.context) + return SpacesViewHolder(binding) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val spacesViewHolder = holder as SpacesViewHolder + spacesViewHolder.binding.apply { + val space = spacesList[position] + + spacesListItemName.text = space.name + spacesListItemSubtitle.text = space.description + + val spaceSpecialImage = space.getSpaceSpecialImage() + spacesListItemImage.tag = spaceSpecialImage?.id + + if (spaceSpecialImage != null) { + val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(spaceSpecialImage.id) + if (thumbnail != null) { + spacesListItemImage.run { + setImageBitmap(thumbnail) + scaleType = ImageView.ScaleType.CENTER_CROP + } + } + if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(spaceSpecialImage, spacesListItemImage)) { + val account = AccountUtils.getOwnCloudAccountByName(spacesViewHolder.itemView.context, space.accountName) + val task = ThumbnailsCacheManager.ThumbnailGenerationTask(spacesListItemImage, account) + val asyncDrawable = ThumbnailsCacheManager.AsyncThumbnailDrawable(spacesViewHolder.itemView.resources, thumbnail, task) + + // If drawable is not visible, do not update it. + if (asyncDrawable.minimumHeight > 0 && asyncDrawable.minimumWidth > 0) { + spacesListItemImage.run { + spacesListItemImage.setImageDrawable(asyncDrawable) + scaleType = ImageView.ScaleType.CENTER_CROP + } + } + task.execute(spaceSpecialImage) + } + if (spaceSpecialImage.file.mimeType == "image/png") { + spacesListItemImage.setBackgroundColor(ContextCompat.getColor(spacesViewHolder.itemView.context, R.color.background_color)) + } + } + } + } + + fun setData(spaces: List) { + // Let's filter the ones that are disabled for the moment. We may show them as disabled in the future. + val onlyEnabledSpaces = spaces.filterNot { it.isDisabled } + val diffCallback = SpacesListDiffUtil(spacesList, onlyEnabledSpaces) + val diffResult = DiffUtil.calculateDiff(diffCallback) + spacesList.clear() + spacesList.addAll(onlyEnabledSpaces) + diffResult.dispatchUpdatesTo(this) + } + + override fun getItemCount(): Int = spacesList.size + + fun getItem(position: Int) = spacesList[position] + + class SpacesViewHolder(val binding: SpacesListItemBinding) : RecyclerView.ViewHolder(binding.root) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt new file mode 100644 index 00000000000..532c59f5a0a --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListDiffUtil.kt @@ -0,0 +1,52 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2023 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.spaces + +import androidx.recyclerview.widget.DiffUtil +import com.owncloud.android.domain.spaces.model.OCSpace + +class SpacesListDiffUtil( + private val oldList: List, + private val newList: List +) : DiffUtil.Callback() { + override fun getOldListSize(): Int = oldList.size + + override fun getNewListSize(): Int = newList.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + + if ((oldItem.name != newItem.name) || (oldItem.description != newItem.description) || + (oldItem.getSpaceSpecialImage()?.id != newItem.getSpaceSpecialImage()?.id)) { + return false + } + + return true + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt index 8f8f7ed28f4..d2140a26f31 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListFragment.kt @@ -26,16 +26,26 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.GridLayoutManager +import com.owncloud.android.R import com.owncloud.android.databinding.SpacesListFragmentBinding import com.owncloud.android.domain.files.model.FileListOption +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.extensions.collectLatestLifecycleFlow +import com.owncloud.android.extensions.showErrorInSnackbar import com.owncloud.android.extensions.toDrawableRes import com.owncloud.android.extensions.toSubtitleStringRes import com.owncloud.android.extensions.toTitleStringRes +import org.koin.androidx.viewmodel.ext.android.viewModel class SpacesListFragment : Fragment() { private var _binding: SpacesListFragmentBinding? = null private val binding get() = _binding!! + private val spacesListViewModel: SpacesListViewModel by viewModel() + + private lateinit var spacesListAdapter: SpacesListAdapter + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -46,15 +56,35 @@ class SpacesListFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - showOrHideEmptyView() + initViews() + subscribeToViewModels() + } + + private fun initViews() { + val spacesListLayoutManager = GridLayoutManager(requireContext(), 2) + binding.recyclerSpacesList.layoutManager = spacesListLayoutManager + spacesListAdapter = SpacesListAdapter() + binding.recyclerSpacesList.adapter = spacesListAdapter + + binding.swipeRefreshSpacesList.setOnRefreshListener { + spacesListViewModel.refreshSpacesFromServer() + } + } + + private fun subscribeToViewModels() { + collectLatestLifecycleFlow(spacesListViewModel.spacesList) { uiState -> + showOrHideEmptyView(uiState.spaces) + spacesListAdapter.setData(uiState.spaces) + binding.swipeRefreshSpacesList.isRefreshing = uiState.refreshing + uiState.error?.let { showErrorInSnackbar(R.string.spaces_sync_failed, it) } + } } - // TODO: Use this method only when necessary, for the moment the empty view is shown always - private fun showOrHideEmptyView() { - binding.recyclerSpacesList.isVisible = false + private fun showOrHideEmptyView(spacesList: List) { + binding.recyclerSpacesList.isVisible = spacesList.isNotEmpty() with(binding.emptyDataParent) { - root.isVisible = true + root.isVisible = spacesList.isEmpty() listEmptyDatasetIcon.setImageResource(FileListOption.SPACES_LIST.toDrawableRes()) listEmptyDatasetTitle.setText(FileListOption.SPACES_LIST.toTitleStringRes()) listEmptyDatasetSubTitle.setText(FileListOption.SPACES_LIST.toSubtitleStringRes()) diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt new file mode 100644 index 00000000000..d2741445611 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/spaces/SpacesListViewModel.kt @@ -0,0 +1,73 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.presentation.spaces + +import android.accounts.Account +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.owncloud.android.domain.UseCaseResult +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.usecases.GetProjectSpacesWithSpecialsForAccountAsStreamUseCase +import com.owncloud.android.domain.spaces.usecases.RefreshSpacesFromServerAsyncUseCase +import com.owncloud.android.providers.CoroutinesDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class SpacesListViewModel( + private val refreshSpacesFromServerAsyncUseCase: RefreshSpacesFromServerAsyncUseCase, + private val getProjectSpacesWithSpecialsForAccountAsStreamUseCase: GetProjectSpacesWithSpecialsForAccountAsStreamUseCase, + private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, + private val account: Account, +) : ViewModel() { + + private val _spacesList: MutableStateFlow = + MutableStateFlow(SpacesListUiState(spaces = emptyList(), refreshing = false, error = null)) + val spacesList: StateFlow = _spacesList + + init { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + refreshSpacesFromServer() + getProjectSpacesWithSpecialsForAccountAsStreamUseCase.execute( + GetProjectSpacesWithSpecialsForAccountAsStreamUseCase.Params(accountName = account.name) + ).collect { spaces -> + _spacesList.update { it.copy(spaces = spaces) } + } + } + } + + fun refreshSpacesFromServer() { + viewModelScope.launch(coroutinesDispatcherProvider.io) { + _spacesList.update { it.copy(refreshing = true) } + when (val result = refreshSpacesFromServerAsyncUseCase.execute(RefreshSpacesFromServerAsyncUseCase.Params(account.name))) { + is UseCaseResult.Success -> _spacesList.update { it.copy(refreshing = false, error = null) } + is UseCaseResult.Error -> _spacesList.update { it.copy(refreshing = false, error = result.throwable) } + } + } + } + + data class SpacesListUiState( + val spaces: List, + val refreshing: Boolean, + val error: Throwable?, + ) +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt index 5be87abac39..0033b651799 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.kt @@ -224,9 +224,9 @@ abstract class DrawerActivity : ToolbarActivity() { // Allow or disallow touches with other visible windows getBottomNavigationView()?.filterTouchesWhenObscured = PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(this) if (account != null) { - capabilitiesViewModel.capabilities.observe(this, Event.EventObserver { uiResult: UIResult -> - setSpacesVisibilityBottomBar(uiResult) - }) + capabilitiesViewModel.capabilities.observe(this) { event: Event> -> + setSpacesVisibilityBottomBar(event.peekContent()) + } } setCheckedItemAtBottomBar(menuItemId) getBottomNavigationView()?.setOnNavigationItemSelectedListener { menuItem: MenuItem -> diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index bf1954ea694..11b736c001a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -863,7 +863,7 @@ class FileDisplayActivity : FileActivity(), FileListOption.ALL_FILES -> getString(R.string.default_display_name_for_root_folder) FileListOption.SPACES_LIST -> getString(R.string.drawer_item_spaces) } - setupRootToolbar(title, isSearchEnabled = true) + setupRootToolbar(title, isSearchEnabled = fileListOption != FileListOption.SPACES_LIST) listMainFileFragment?.setSearchListener(findViewById(R.id.root_toolbar_search_view)) } else { updateStandardToolbar(title = chosenFile.fileName, displayHomeAsUpEnabled = true, homeButtonEnabled = true) diff --git a/owncloudApp/src/main/res/drawable/ic_spaces.xml b/owncloudApp/src/main/res/drawable/ic_spaces.xml index f7893d63d89..1d062030439 100644 --- a/owncloudApp/src/main/res/drawable/ic_spaces.xml +++ b/owncloudApp/src/main/res/drawable/ic_spaces.xml @@ -1,9 +1,4 @@ - - + + diff --git a/owncloudApp/src/main/res/layout/spaces_list_fragment.xml b/owncloudApp/src/main/res/layout/spaces_list_fragment.xml index 1ee3a007d50..e7b09bc4aa5 100644 --- a/owncloudApp/src/main/res/layout/spaces_list_fragment.xml +++ b/owncloudApp/src/main/res/layout/spaces_list_fragment.xml @@ -1,4 +1,5 @@ - + + + + + + + + + + + + + + + + + + + + diff --git a/owncloudApp/src/main/res/values/colors.xml b/owncloudApp/src/main/res/values/colors.xml index 1473e10e5c9..d34da81229f 100644 --- a/owncloudApp/src/main/res/values/colors.xml +++ b/owncloudApp/src/main/res/values/colors.xml @@ -67,4 +67,4 @@ #6E758C #6E758C - \ No newline at end of file + diff --git a/owncloudApp/src/main/res/values/setup.xml b/owncloudApp/src/main/res/values/setup.xml index ac582d36a12..f2bba3ce67b 100644 --- a/owncloudApp/src/main/res/values/setup.xml +++ b/owncloudApp/src/main/res/values/setup.xml @@ -45,6 +45,7 @@ #D6D7D7 @color/black @color/owncloud_blue_accent + #edf3fa @color/owncloud_blue diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 999035b3ee9..4022232a845 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -672,6 +672,7 @@ Logs may contain sensitive information. Sharing logs with others is sole user responsibility Synchronizing account + Spaces could not be refreshed Found %1$s of data on your external storage. It will be moved to the safe storage on your device. Remaining files will be cleaned from your external storage after the migration to avoid duplicates and vulnerability. diff --git a/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/40.json b/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/40.json index 10a6b97f5d4..822ae83b93c 100644 --- a/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/40.json +++ b/owncloudData/schemas/com.owncloud.android.data.OwncloudDatabase/40.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 40, - "identityHash": "be3530216034282e4a6680bf121e3096", + "identityHash": "50fbdf2d302ff496a8c428b44ea22665", "entities": [ { "tableName": "folder_backup", @@ -651,38 +651,6 @@ "indices": [], "foreignKeys": [] }, - { - "tableName": "user_quotas", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))", - "fields": [ - { - "fieldPath": "accountName", - "columnName": "accountName", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "used", - "columnName": "used", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "available", - "columnName": "available", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "accountName" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, { "tableName": "transfers", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localPath` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `accountName` TEXT NOT NULL, `fileSize` INTEGER NOT NULL, `status` INTEGER NOT NULL, `localBehaviour` INTEGER NOT NULL, `forceOverwrite` INTEGER NOT NULL, `transferEndTimestamp` INTEGER, `lastResult` INTEGER, `createdBy` INTEGER NOT NULL, `transferId` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", @@ -768,12 +736,250 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "spaces", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`account_name` TEXT NOT NULL, `drive_alias` TEXT NOT NULL, `drive_type` TEXT NOT NULL, `space_id` TEXT NOT NULL, `last_modified_date_time` TEXT NOT NULL, `name` TEXT NOT NULL, `owner_id` TEXT NOT NULL, `web_url` TEXT NOT NULL, `description` TEXT, `quota_remaining` INTEGER, `quota_state` TEXT, `quota_total` INTEGER, `quota_used` INTEGER, `root_etag` TEXT, `root_id` TEXT, `root_web_dav_url` TEXT, `root_deleted_state` TEXT, PRIMARY KEY(`account_name`, `space_id`))", + "fields": [ + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "driveAlias", + "columnName": "drive_alias", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "driveType", + "columnName": "drive_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "space_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModifiedDateTime", + "columnName": "last_modified_date_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "webUrl", + "columnName": "web_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quota.remaining", + "columnName": "quota_remaining", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quota.state", + "columnName": "quota_state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quota.total", + "columnName": "quota_total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quota.used", + "columnName": "quota_used", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "root.eTag", + "columnName": "root_etag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "root.id", + "columnName": "root_id", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "root.webDavUrl", + "columnName": "root_web_dav_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "root.deleteState", + "columnName": "root_deleted_state", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "account_name", + "space_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "spaces_special", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`spaces_special_account_name` TEXT NOT NULL, `spaces_special_space_id` TEXT NOT NULL, `spaces_special_etag` TEXT NOT NULL, `file_mime_type` TEXT NOT NULL, `special_id` TEXT NOT NULL, `last_modified_date_time` TEXT NOT NULL, `name` TEXT NOT NULL, `size` INTEGER NOT NULL, `special_folder_name` TEXT NOT NULL, `special_web_dav_url` TEXT NOT NULL, PRIMARY KEY(`spaces_special_space_id`, `special_id`), FOREIGN KEY(`spaces_special_account_name`, `spaces_special_space_id`) REFERENCES `spaces`(`account_name`, `space_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "accountName", + "columnName": "spaces_special_account_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "spaceId", + "columnName": "spaces_special_space_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "spaces_special_etag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fileMimeType", + "columnName": "file_mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "special_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastModifiedDateTime", + "columnName": "last_modified_date_time", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "specialFolderName", + "columnName": "special_folder_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "webDavUrl", + "columnName": "special_web_dav_url", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "spaces_special_space_id", + "special_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "spaces", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "spaces_special_account_name", + "spaces_special_space_id" + ], + "referencedColumns": [ + "account_name", + "space_id" + ] + } + ] + }, + { + "tableName": "user_quotas", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL, `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))", + "fields": [ + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "used", + "columnName": "used", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "accountName" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] } ], "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be3530216034282e4a6680bf121e3096')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '50fbdf2d302ff496a8c428b44ea22665')" ] } } \ No newline at end of file diff --git a/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt b/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt index 8ab36962772..b2a088d9c6d 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/OwncloudDatabase.kt @@ -48,6 +48,9 @@ import com.owncloud.android.data.migrations.MIGRATION_35_36 import com.owncloud.android.data.migrations.MIGRATION_37_38 import com.owncloud.android.data.sharing.shares.db.OCShareDao import com.owncloud.android.data.sharing.shares.db.OCShareEntity +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity +import com.owncloud.android.data.spaces.db.SpacesDao +import com.owncloud.android.data.spaces.db.SpacesEntity import com.owncloud.android.data.transfers.db.OCTransferEntity import com.owncloud.android.data.transfers.db.TransferDao import com.owncloud.android.data.user.db.UserDao @@ -60,8 +63,10 @@ import com.owncloud.android.data.user.db.UserQuotaEntity OCFileEntity::class, OCFileSyncEntity::class, OCShareEntity::class, - UserQuotaEntity::class, OCTransferEntity::class, + SpacesEntity::class, + SpaceSpecialEntity::class, + UserQuotaEntity::class, ], autoMigrations = [ AutoMigration(from = 36, to = 37), @@ -76,8 +81,9 @@ abstract class OwncloudDatabase : RoomDatabase() { abstract fun fileDao(): FileDao abstract fun folderBackUpDao(): FolderBackupDao abstract fun shareDao(): OCShareDao - abstract fun userDao(): UserDao + abstract fun spacesDao(): SpacesDao abstract fun transferDao(): TransferDao + abstract fun userDao(): UserDao companion object { @Volatile diff --git a/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java b/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java index fb491176c90..525983566d1 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java +++ b/owncloudData/src/main/java/com/owncloud/android/data/ProviderMeta.java @@ -37,105 +37,102 @@ private ProviderMeta() { } static public class ProviderTableMeta implements BaseColumns { - public static final String OCSHARES_TABLE_NAME = "ocshares"; public static final String CAPABILITIES_TABLE_NAME = "capabilities"; + public static final String FILES_SYNC_TABLE_NAME = "files_sync"; public static final String FILES_TABLE_NAME = "files"; - public static final String USER_QUOTAS_TABLE_NAME = "user_quotas"; public static final String FOLDER_BACKUP_TABLE_NAME = "folder_backup"; + public static final String OCSHARES_TABLE_NAME = "ocshares"; + public static final String SPACES_TABLE_NAME = "spaces"; + public static final String SPACES_SPECIAL_TABLE_NAME = "spaces_special"; public static final String TRANSFERS_TABLE_NAME = "transfers"; - public static final String FILES_SYNC_TABLE_NAME = "files_sync"; + public static final String USER_QUOTAS_TABLE_NAME = "user_quotas"; // Columns of ocshares table - public static final String OCSHARES_SHARE_TYPE = "share_type"; - public static final String OCSHARES_SHARE_WITH = "share_with"; + public static final String OCSHARES_ACCOUNT_OWNER = "owner_share"; + public static final String OCSHARES_EXPIRATION_DATE = "expiration_date"; + public static final String OCSHARES_ID_REMOTE_SHARED = "id_remote_shared"; + public static final String OCSHARES_IS_DIRECTORY = "is_directory"; + public static final String OCSHARES_NAME = "name"; public static final String OCSHARES_PATH = "path"; public static final String OCSHARES_PERMISSIONS = "permissions"; public static final String OCSHARES_SHARED_DATE = "shared_date"; - public static final String OCSHARES_EXPIRATION_DATE = "expiration_date"; - public static final String OCSHARES_TOKEN = "token"; - public static final String OCSHARES_SHARE_WITH_DISPLAY_NAME = "shared_with_display_name"; + public static final String OCSHARES_SHARE_TYPE = "share_type"; + public static final String OCSHARES_SHARE_WITH = "share_with"; public static final String OCSHARES_SHARE_WITH_ADDITIONAL_INFO = "share_with_additional_info"; - public static final String OCSHARES_IS_DIRECTORY = "is_directory"; - public static final String OCSHARES_ID_REMOTE_SHARED = "id_remote_shared"; - public static final String OCSHARES_ACCOUNT_OWNER = "owner_share"; - public static final String OCSHARES_NAME = "name"; + public static final String OCSHARES_SHARE_WITH_DISPLAY_NAME = "shared_with_display_name"; + public static final String OCSHARES_TOKEN = "token"; public static final String OCSHARES_URL = "url"; // Columns of capabilities table public static final String CAPABILITIES_ACCOUNT_NAME = "account"; - public static final String LEGACY_CAPABILITIES_VERSION_MAYOR = "version_mayor"; - public static final String CAPABILITIES_VERSION_MAJOR = "version_major"; - public static final String CAPABILITIES_VERSION_MINOR = "version_minor"; - public static final String CAPABILITIES_VERSION_MICRO = "version_micro"; - public static final String CAPABILITIES_VERSION_STRING = "version_string"; - public static final String CAPABILITIES_VERSION_EDITION = "version_edition"; + public static final String CAPABILITIES_APP_PROVIDERS_PREFIX = "app_providers_"; public static final String CAPABILITIES_CORE_POLLINTERVAL = "core_pollinterval"; public static final String CAPABILITIES_DAV_CHUNKING_VERSION = "dav_chunking_version"; + public static final String CAPABILITIES_FILES_APP_PROVIDERS = "files_apps_providers"; + public static final String CAPABILITIES_FILES_BIGFILECHUNKING = "files_bigfilechunking"; + public static final String CAPABILITIES_FILES_PRIVATE_LINKS = "files_private_links"; + public static final String CAPABILITIES_FILES_UNDELETE = "files_undelete"; + public static final String CAPABILITIES_FILES_VERSIONING = "files_versioning"; public static final String CAPABILITIES_SHARING_API_ENABLED = "sharing_api_enabled"; + public static final String CAPABILITIES_SHARING_FEDERATION_INCOMING = "sharing_federation_incoming"; + public static final String CAPABILITIES_SHARING_FEDERATION_OUTGOING = "sharing_federation_outgoing"; public static final String CAPABILITIES_SHARING_PUBLIC_ENABLED = "sharing_public_enabled"; - public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED = "sharing_public_password_enforced"; - public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY = - "sharing_public_password_enforced_read_only"; - public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE = - "sharing_public_password_enforced_read_write"; - public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY = - "sharing_public_password_enforced_public_only"; - public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED = - "sharing_public_expire_date_enabled"; - public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS = - "sharing_public_expire_date_days"; - public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED = - "sharing_public_expire_date_enforced"; - public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload"; + public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS = "sharing_public_expire_date_days"; + public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED = "sharing_public_expire_date_enabled"; + public static final String CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED = "sharing_public_expire_date_enforced"; public static final String CAPABILITIES_SHARING_PUBLIC_MULTIPLE = "sharing_public_multiple"; + public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED = "sharing_public_password_enforced"; + public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY = "sharing_public_password_enforced_read_only"; + public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE = "sharing_public_password_enforced_read_write"; + public static final String CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY = "sharing_public_password_enforced_public_only"; public static final String CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY = "supports_upload_only"; + public static final String CAPABILITIES_SHARING_PUBLIC_UPLOAD = "sharing_public_upload"; public static final String CAPABILITIES_SHARING_RESHARING = "sharing_resharing"; - public static final String CAPABILITIES_SHARING_FEDERATION_OUTGOING = "sharing_federation_outgoing"; - public static final String CAPABILITIES_SHARING_FEDERATION_INCOMING = "sharing_federation_incoming"; public static final String CAPABILITIES_SHARING_USER_PROFILE_PICTURE = "sharing_user_profile_picture"; - public static final String CAPABILITIES_FILES_BIGFILECHUNKING = "files_bigfilechunking"; - public static final String CAPABILITIES_FILES_UNDELETE = "files_undelete"; - public static final String CAPABILITIES_FILES_VERSIONING = "files_versioning"; - public static final String CAPABILITIES_FILES_PRIVATE_LINKS = "files_private_links"; - public static final String CAPABILITIES_APP_PROVIDERS_PREFIX = "app_providers_"; public static final String CAPABILITIES_SPACES_PREFIX = "spaces_"; + public static final String CAPABILITIES_VERSION_EDITION = "version_edition"; + public static final String CAPABILITIES_VERSION_MAJOR = "version_major"; + public static final String CAPABILITIES_VERSION_MICRO = "version_micro"; + public static final String CAPABILITIES_VERSION_MINOR = "version_minor"; + public static final String CAPABILITIES_VERSION_STRING = "version_string"; + public static final String LEGACY_CAPABILITIES_VERSION_MAYOR = "version_mayor"; // Columns of filelist table - public static final String FILE_PARENT = "parent"; - public static final String FILE_NAME = "filename"; - public static final String FILE_CREATION = "created"; - public static final String FILE_MODIFIED = "modified"; - public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data"; + public static final String FILE_ACCOUNT_OWNER = "file_owner"; public static final String FILE_CONTENT_LENGTH = "content_length"; public static final String FILE_CONTENT_TYPE = "content_type"; - public static final String FILE_STORAGE_PATH = "media_path"; - public static final String FILE_PATH = "path"; - public static final String FILE_ACCOUNT_OWNER = "file_owner"; + public static final String FILE_CREATION = "created"; + public static final String FILE_ETAG = "etag"; + public static final String FILE_ETAG_IN_CONFLICT = "etag_in_conflict"; + public static final String FILE_IS_DOWNLOADING = "is_downloading"; + public static final String FILE_KEEP_IN_SYNC = "keep_in_sync"; public static final String FILE_LAST_SYNC_DATE = "last_sync_date";// _for_properties, but let's keep it as it is public static final String FILE_LAST_SYNC_DATE_FOR_DATA = "last_sync_date_for_data"; - public static final String FILE_KEEP_IN_SYNC = "keep_in_sync"; - public static final String FILE_ETAG = "etag"; - public static final String FILE_TREE_ETAG = "tree_etag"; - public static final String FILE_SHARED_VIA_LINK = "share_by_link"; - public static final String FILE_SHARED_WITH_SHAREE = "shared_via_users"; + public static final String FILE_MODIFIED = "modified"; + public static final String FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA = "modified_at_last_sync_for_data"; + public static final String FILE_NAME = "filename"; + public static final String FILE_PARENT = "parent"; + public static final String FILE_PATH = "path"; public static final String FILE_PERMISSIONS = "permissions"; + public static final String FILE_PRIVATE_LINK = "private_link"; public static final String FILE_REMOTE_ID = "remote_id"; + public static final String FILE_SHARED_VIA_LINK = "share_by_link"; + public static final String FILE_SHARED_WITH_SHAREE = "shared_via_users"; + public static final String FILE_STORAGE_PATH = "media_path"; + public static final String FILE_TREE_ETAG = "tree_etag"; public static final String FILE_UPDATE_THUMBNAIL = "update_thumbnail"; - public static final String FILE_IS_DOWNLOADING = "is_downloading"; - public static final String FILE_ETAG_IN_CONFLICT = "etag_in_conflict"; - public static final String FILE_PRIVATE_LINK = "private_link"; // Columns of list_of_uploads table - public static final String UPLOAD_LOCAL_PATH = "local_path"; - public static final String UPLOAD_REMOTE_PATH = "remote_path"; public static final String UPLOAD_ACCOUNT_NAME = "account_name"; + public static final String UPLOAD_CREATED_BY = "created_by"; public static final String UPLOAD_FILE_SIZE = "file_size"; - public static final String UPLOAD_STATUS = "status"; - public static final String UPLOAD_LOCAL_BEHAVIOUR = "local_behaviour"; public static final String UPLOAD_FORCE_OVERWRITE = "force_overwrite"; - public static final String UPLOAD_UPLOAD_END_TIMESTAMP = "upload_end_timestamp"; public static final String UPLOAD_LAST_RESULT = "last_result"; - public static final String UPLOAD_CREATED_BY = "created_by"; + public static final String UPLOAD_LOCAL_BEHAVIOUR = "local_behaviour"; + public static final String UPLOAD_LOCAL_PATH = "local_path"; + public static final String UPLOAD_REMOTE_PATH = "remote_path"; + public static final String UPLOAD_STATUS = "status"; public static final String UPLOAD_TRANSFER_ID = "transfer_id"; + public static final String UPLOAD_UPLOAD_END_TIMESTAMP = "upload_end_timestamp"; } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/LocalSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/LocalSpacesDataSource.kt new file mode 100644 index 00000000000..c5b470f72a6 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/LocalSpacesDataSource.kt @@ -0,0 +1,30 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.spaces.datasources + +import com.owncloud.android.domain.spaces.model.OCSpace +import kotlinx.coroutines.flow.Flow + +interface LocalSpacesDataSource { + fun saveSpacesForAccount(listOfSpaces: List) + fun getProjectSpacesWithSpecialsForAccountAsFlow(accountName: String): Flow> +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt new file mode 100644 index 00000000000..1f06f414551 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/RemoteSpacesDataSource.kt @@ -0,0 +1,25 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.spaces.datasources + +import com.owncloud.android.domain.spaces.model.OCSpace + +interface RemoteSpacesDataSource { + fun refreshSpacesForAccount(accountName: String): List +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCLocalSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCLocalSpacesDataSource.kt new file mode 100644 index 00000000000..3d36f9fe3f6 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCLocalSpacesDataSource.kt @@ -0,0 +1,154 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.spaces.datasources.implementation + +import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource +import com.owncloud.android.data.spaces.db.SpaceQuotaEntity +import com.owncloud.android.data.spaces.db.SpaceRootEntity +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity +import com.owncloud.android.data.spaces.db.SpacesDao +import com.owncloud.android.data.spaces.db.SpacesEntity +import com.owncloud.android.data.spaces.db.SpacesWithSpecials +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceDeleted +import com.owncloud.android.domain.spaces.model.SpaceFile +import com.owncloud.android.domain.spaces.model.SpaceOwner +import com.owncloud.android.domain.spaces.model.SpaceQuota +import com.owncloud.android.domain.spaces.model.SpaceRoot +import com.owncloud.android.domain.spaces.model.SpaceSpecial +import com.owncloud.android.domain.spaces.model.SpaceSpecialFolder +import com.owncloud.android.domain.spaces.model.SpaceUser +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class OCLocalSpacesDataSource( + private val spacesDao: SpacesDao, +) : LocalSpacesDataSource { + + override fun saveSpacesForAccount(listOfSpaces: List) { + val spaceEntities = mutableListOf() + val spaceSpecialEntities = mutableListOf() + + listOfSpaces.forEach { spaceModel -> + spaceEntities.add(spaceModel.toEntity()) + spaceModel.special?.let { listOfSpacesSpecials -> + spaceSpecialEntities.addAll(listOfSpacesSpecials.map { it.toEntity(spaceModel.accountName, spaceModel.id) }) + } + } + + spacesDao.insertOrDeleteSpaces(spaceEntities, spaceSpecialEntities) + } + + override fun getProjectSpacesWithSpecialsForAccountAsFlow(accountName: String): Flow> { + return spacesDao.getProjectSpacesWithSpecialsForAccountAsFlow(accountName).map { spacesWithSpecialsEntitiesList -> + spacesWithSpecialsEntitiesList.map { spacesWithSpecialsEntity -> + spacesWithSpecialsEntity.toModel() + } + } + } + + private fun SpacesWithSpecials.toModel() = + OCSpace( + accountName = space.accountName, + driveAlias = space.driveAlias, + driveType = space.driveType, + id = space.id, + lastModifiedDateTime = space.lastModifiedDateTime, + name = space.name, + owner = SpaceOwner( + user = SpaceUser( + id = space.ownerId + ) + ), + quota = space.quota?.let { spaceQuotaEntity -> + SpaceQuota( + remaining = spaceQuotaEntity.remaining, + state = spaceQuotaEntity.state, + total = spaceQuotaEntity.total, + used = spaceQuotaEntity.used + ) + }, + root = space.root!!.let { spaceRootEntity -> + SpaceRoot( + eTag = spaceRootEntity.eTag, + id = spaceRootEntity.id, + permissions = null, + webDavUrl = spaceRootEntity.webDavUrl, + deleted = spaceRootEntity.deleteState?.let { SpaceDeleted(state = it) } + ) + }, + webUrl = space.webUrl, + description = space.description, + special = specials.map { + it.toModel() + } + ) + + private fun SpaceSpecialEntity.toModel() = + SpaceSpecial( + eTag = eTag, + file = SpaceFile( + mimeType = fileMimeType + ), + id = id, + lastModifiedDateTime = lastModifiedDateTime, + name = name, + size = size, + specialFolder = SpaceSpecialFolder( + name = specialFolderName + ), + webDavUrl = webDavUrl + ) + + private fun OCSpace.toEntity() = + SpacesEntity( + accountName = accountName, + driveAlias = driveAlias, + driveType = driveType, + id = id, + lastModifiedDateTime = lastModifiedDateTime, + name = name, + ownerId = owner.user.id, + quota = quota?.let { quotaModel -> + SpaceQuotaEntity(remaining = quotaModel.remaining, state = quotaModel.state, total = quotaModel.total, used = quotaModel.used) + }, + root = root.let { rootModel -> + SpaceRootEntity(eTag = rootModel.eTag, id = rootModel.id, webDavUrl = rootModel.webDavUrl, deleteState = rootModel.deleted?.state) + }, + webUrl = webUrl, + description = description, + ) + + private fun SpaceSpecial.toEntity(accountName: String, spaceId: String): SpaceSpecialEntity = + SpaceSpecialEntity( + accountName = accountName, + spaceId = spaceId, + eTag = eTag, + fileMimeType = file.mimeType, + id = id, + lastModifiedDateTime = lastModifiedDateTime, + name = name, + size = size, + specialFolderName = specialFolder.name, + webDavUrl = webDavUrl + ) +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt new file mode 100644 index 00000000000..3e4f7e28aec --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/datasources/implementation/OCRemoteSpacesDataSource.kt @@ -0,0 +1,98 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.data.spaces.datasources.implementation + +import com.owncloud.android.data.executeRemoteOperation +import com.owncloud.android.data.spaces.datasources.RemoteSpacesDataSource +import com.owncloud.android.domain.spaces.model.OCSpace +import com.owncloud.android.domain.spaces.model.SpaceDeleted +import com.owncloud.android.domain.spaces.model.SpaceFile +import com.owncloud.android.domain.spaces.model.SpaceGrantedTo +import com.owncloud.android.domain.spaces.model.SpaceOwner +import com.owncloud.android.domain.spaces.model.SpacePermission +import com.owncloud.android.domain.spaces.model.SpaceQuota +import com.owncloud.android.domain.spaces.model.SpaceRoot +import com.owncloud.android.domain.spaces.model.SpaceSpecial +import com.owncloud.android.domain.spaces.model.SpaceSpecialFolder +import com.owncloud.android.domain.spaces.model.SpaceUser +import com.owncloud.android.lib.resources.spaces.responses.SpaceResponse +import com.owncloud.android.lib.resources.spaces.services.SpacesService + +class OCRemoteSpacesDataSource( + private val spacesService: SpacesService +) : RemoteSpacesDataSource { + override fun refreshSpacesForAccount(accountName: String): List { + val spacesResponse = executeRemoteOperation { + spacesService.getSpaces() + } + + return spacesResponse.map { it.toModel(accountName) } + } + + private fun SpaceResponse.toModel(accountName: String): OCSpace = + OCSpace( + accountName = accountName, + driveAlias = driveAlias, + driveType = driveType, + id = id, + lastModifiedDateTime = lastModifiedDateTime, + name = name, + owner = SpaceOwner( + user = SpaceUser( + id = owner.user.id + ) + ), + quota = quota?.let { quotaResponse -> + SpaceQuota( + remaining = quotaResponse.remaining, + state = quotaResponse.state, + total = quotaResponse.total, + used = quotaResponse.used, + ) + }, + root = SpaceRoot( + eTag = root.eTag, + id = root.id, + permissions = root.permissions?.map { permissionsResponse -> + SpacePermission( + grantedTo = permissionsResponse.grantedTo.map { grantedToResponse -> + SpaceGrantedTo(SpaceUser(grantedToResponse.user.id)) + }, + roles = permissionsResponse.roles, + ) + }, + webDavUrl = root.webDavUrl, + deleted = root.deleted?.let { SpaceDeleted(state = it.state) } + ), + webUrl = webUrl, + description = description, + special = special?.map { specialResponse -> + SpaceSpecial( + eTag = specialResponse.eTag, + file = SpaceFile(mimeType = specialResponse.file.mimeType), + id = specialResponse.id, + lastModifiedDateTime = specialResponse.lastModifiedDateTime, + name = specialResponse.name, + size = specialResponse.size, + specialFolder = SpaceSpecialFolder(name = specialResponse.specialFolder.name), + webDavUrl = specialResponse.webDavUrl + ) + } + ) +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpaceSpecialEntity.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpaceSpecialEntity.kt new file mode 100644 index 00000000000..15c38bcac82 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpaceSpecialEntity.kt @@ -0,0 +1,74 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.spaces.db + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import com.owncloud.android.data.ProviderMeta +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ACCOUNT_NAME +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ID +import com.owncloud.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_SPACE_ID +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_LAST_MODIFIED_DATE_TIME + +@Entity( + tableName = ProviderMeta.ProviderTableMeta.SPACES_SPECIAL_TABLE_NAME, + primaryKeys = [SPACES_SPECIAL_SPACE_ID, SPACES_SPECIAL_ID], + foreignKeys = [ForeignKey( + entity = SpacesEntity::class, + parentColumns = arrayOf(SPACES_ACCOUNT_NAME, SPACES_ID), + childColumns = arrayOf(SPACES_SPECIAL_ACCOUNT_NAME, SPACES_SPECIAL_SPACE_ID), + onDelete = ForeignKey.CASCADE + )] +) +data class SpaceSpecialEntity( + @ColumnInfo(name = SPACES_SPECIAL_ACCOUNT_NAME) + val accountName: String, + @ColumnInfo(name = SPACES_SPECIAL_SPACE_ID) + val spaceId: String, + @ColumnInfo(name = SPACES_SPECIAL_ETAG) + val eTag: String, + @ColumnInfo(name = SPACES_SPECIAL_FILE_MIME_TYPE) + val fileMimeType: String, + @ColumnInfo(name = SPACES_SPECIAL_ID) + val id: String, + @ColumnInfo(name = SPACES_LAST_MODIFIED_DATE_TIME) + val lastModifiedDateTime: String, + val name: String, + val size: Int, + @ColumnInfo(name = SPACES_SPECIAL_FOLDER_NAME) + val specialFolderName: String, + @ColumnInfo(name = SPACES_SPECIAL_WEB_DAV_URL) + val webDavUrl: String +) { + companion object { + const val SPACES_SPECIAL_ACCOUNT_NAME = "spaces_special_account_name" + const val SPACES_SPECIAL_SPACE_ID = "spaces_special_space_id" + const val SPACES_SPECIAL_ETAG = "spaces_special_etag" + const val SPACES_SPECIAL_FILE_MIME_TYPE = "file_mime_type" + const val SPACES_SPECIAL_ID = "special_id" + const val SPACES_SPECIAL_FOLDER_NAME = "special_folder_name" + const val SPACES_SPECIAL_WEB_DAV_URL = "special_web_dav_url" + } +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt new file mode 100644 index 00000000000..8ac89522ae7 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesDao.kt @@ -0,0 +1,122 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.spaces.db + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import com.owncloud.android.data.ProviderMeta +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.DRIVE_TYPE_PERSONAL +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.DRIVE_TYPE_PROJECT +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_DRIVE_TYPE +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID +import kotlinx.coroutines.flow.Flow + +@Dao +interface SpacesDao { + @Transaction + fun insertOrDeleteSpaces( + listOfSpacesEntities: List, + listOfSpecialEntities: List, + ) { + val currentAccountName = listOfSpacesEntities.first().accountName + val currentSpaces = getAllSpacesForAccount(currentAccountName) + + // Delete spaces that are not attached to the current account anymore + val spacesToDelete = currentSpaces.filterNot { oldSpace -> + listOfSpacesEntities.any { it.id == oldSpace.id } + } + + spacesToDelete.forEach { spaceToDelete -> + deleteSpaceForAccountById(accountName = spaceToDelete.accountName, spaceId = spaceToDelete.id) + } + + // Insert new spaces + insertOrReplaceSpaces(listOfSpacesEntities) + insertOrReplaceSpecials(listOfSpecialEntities) + } + + // TODO: Use upsert instead of insert and replace + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplaceSpaces(listOfSpacesEntities: List): List + + // TODO: Use upsert instead of insert and replace + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplaceSpecials(listOfSpecialEntities: List): List + + @Query(SELECT_ALL_SPACES_FOR_ACCOUNT) + fun getAllSpacesForAccount( + accountName: String, + ): List + + @Query(SELECT_PERSONAL_SPACE_FOR_ACCOUNT) + fun getPersonalSpacesForAccount( + accountName: String, + ): List + + @Query(SELECT_PROJECT_SPACES_FOR_ACCOUNT) + fun getProjectSpacesWithSpecialsForAccountAsFlow( + accountName: String, + ): Flow> + + @Query(DELETE_ALL_SPACES_FOR_ACCOUNT) + fun deleteSpacesForAccount(accountName: String) + + @Query(DELETE_SPACE_FOR_ACCOUNT_BY_ID) + fun deleteSpaceForAccountById(accountName: String, spaceId: String) + + companion object { + private const val SELECT_ALL_SPACES_FOR_ACCOUNT = """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} + WHERE $SPACES_ACCOUNT_NAME = :accountName + """ + + private const val SELECT_PERSONAL_SPACE_FOR_ACCOUNT = """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} + WHERE $SPACES_ACCOUNT_NAME = :accountName AND $SPACES_DRIVE_TYPE LIKE '$DRIVE_TYPE_PERSONAL' + """ + + private const val SELECT_PROJECT_SPACES_FOR_ACCOUNT = """ + SELECT * + FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} + WHERE $SPACES_ACCOUNT_NAME = :accountName AND $SPACES_DRIVE_TYPE LIKE '$DRIVE_TYPE_PROJECT' + ORDER BY name COLLATE NOCASE ASC + """ + + private const val DELETE_ALL_SPACES_FOR_ACCOUNT = """ + DELETE + FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} + WHERE $SPACES_ACCOUNT_NAME = :accountName + """ + + private const val DELETE_SPACE_FOR_ACCOUNT_BY_ID = """ + DELETE + FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME} + WHERE $SPACES_ACCOUNT_NAME = :accountName AND $SPACES_ID LIKE :spaceId + """ + } +} diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesEntity.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesEntity.kt new file mode 100644 index 00000000000..d89d9e88bd1 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/db/SpacesEntity.kt @@ -0,0 +1,117 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.spaces.db + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.Relation +import com.owncloud.android.data.ProviderMeta +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_REMAINING +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_STATE +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_TOTAL +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_USED +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_DELETED_STATE +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_ETAG +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_ID +import com.owncloud.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_WEB_DAV_URL + +@Entity( + tableName = ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME, + primaryKeys = [SPACES_ACCOUNT_NAME, SPACES_ID], +) +data class SpacesEntity( + @ColumnInfo(name = SPACES_ACCOUNT_NAME) + val accountName: String, + @ColumnInfo(name = SPACES_DRIVE_ALIAS) + val driveAlias: String, + @ColumnInfo(name = SPACES_DRIVE_TYPE) + val driveType: String, + @ColumnInfo(name = SPACES_ID) + val id: String, + @ColumnInfo(name = SPACES_LAST_MODIFIED_DATE_TIME) + val lastModifiedDateTime: String, + val name: String, + @ColumnInfo(name = SPACES_OWNER_ID) + val ownerId: String, + @Embedded + val quota: SpaceQuotaEntity?, + @Embedded + val root: SpaceRootEntity?, + @ColumnInfo(name = SPACES_WEB_URL) + val webUrl: String, + val description: String?, +) { + + companion object { + const val DRIVE_TYPE_PERSONAL = "personal" + const val DRIVE_TYPE_PROJECT = "project" + const val SPACES_ACCOUNT_NAME = "account_name" + const val SPACES_ID = "space_id" + const val SPACES_DRIVE_ALIAS = "drive_alias" + const val SPACES_DRIVE_TYPE = "drive_type" + const val SPACES_LAST_MODIFIED_DATE_TIME = "last_modified_date_time" + const val SPACES_WEB_URL = "web_url" + const val SPACES_OWNER_ID = "owner_id" + const val SPACES_QUOTA_REMAINING = "quota_remaining" + const val SPACES_QUOTA_STATE = "quota_state" + const val SPACES_QUOTA_TOTAL = "quota_total" + const val SPACES_QUOTA_USED = "quota_used" + const val SPACES_ROOT_ETAG = "root_etag" + const val SPACES_ROOT_ID = "root_id" + const val SPACES_ROOT_WEB_DAV_URL = "root_web_dav_url" + const val SPACES_ROOT_DELETED_STATE = "root_deleted_state" + } +} + +data class SpaceQuotaEntity( + @ColumnInfo(name = SPACES_QUOTA_REMAINING) + val remaining: Long?, + @ColumnInfo(name = SPACES_QUOTA_STATE) + val state: String?, + @ColumnInfo(name = SPACES_QUOTA_TOTAL) + val total: Long, + @ColumnInfo(name = SPACES_QUOTA_USED) + val used: Long?, +) + +data class SpaceRootEntity( + @ColumnInfo(name = SPACES_ROOT_ETAG) + val eTag: String, + @ColumnInfo(name = SPACES_ROOT_ID) + val id: String, + @ColumnInfo(name = SPACES_ROOT_WEB_DAV_URL) + val webDavUrl: String, + @ColumnInfo(name = SPACES_ROOT_DELETED_STATE) + val deleteState: String?, +) + +data class SpacesWithSpecials( + @Embedded val space: SpacesEntity, + @Relation( + parentColumn = SPACES_ID, + entityColumn = SpaceSpecialEntity.SPACES_SPECIAL_SPACE_ID, + ) + val specials: List +) diff --git a/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt new file mode 100644 index 00000000000..5afd4fe8e52 --- /dev/null +++ b/owncloudData/src/main/java/com/owncloud/android/data/spaces/repository/OCSpacesRepository.kt @@ -0,0 +1,40 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.data.spaces.repository + +import com.owncloud.android.data.spaces.datasources.LocalSpacesDataSource +import com.owncloud.android.data.spaces.datasources.RemoteSpacesDataSource +import com.owncloud.android.domain.spaces.SpacesRepository + +class OCSpacesRepository( + private val localSpacesDataSource: LocalSpacesDataSource, + private val remoteSpacesDataSource: RemoteSpacesDataSource, +) : SpacesRepository { + override fun refreshSpacesForAccount(accountName: String) { + remoteSpacesDataSource.refreshSpacesForAccount(accountName).also { listOfSpaces -> + localSpacesDataSource.saveSpacesForAccount(listOfSpaces) + } + } + + override fun getProjectSpacesWithSpecialsForAccountAsFlow(accountName: String) = + localSpacesDataSource.getProjectSpacesWithSpecialsForAccountAsFlow(accountName) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt new file mode 100644 index 00000000000..9f9b480cd40 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/SpacesRepository.kt @@ -0,0 +1,30 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces + +import com.owncloud.android.domain.spaces.model.OCSpace +import kotlinx.coroutines.flow.Flow + +interface SpacesRepository { + fun refreshSpacesForAccount(accountName: String) + fun getProjectSpacesWithSpecialsForAccountAsFlow(accountName: String): Flow> +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt new file mode 100644 index 00000000000..b895e61d9dd --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/model/OCSpace.kt @@ -0,0 +1,110 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.model + +data class OCSpace( + val accountName: String, + val driveAlias: String, + val driveType: String, + val id: String, + val lastModifiedDateTime: String, + val name: String, + val owner: SpaceOwner, + val quota: SpaceQuota?, + val root: SpaceRoot, + val webUrl: String, + val description: String?, + val special: List?, +) { + val isPersonal get() = driveType == DRIVE_TYPE_PERSONAL + val isProject get() = driveType == DRIVE_TYPE_PROJECT + + val isDisabled get() = root.deleted?.state == DRIVE_DISABLED + + fun getSpaceSpecialImage(): SpaceSpecial? { + val imageSpecial = special?.filter { + it.specialFolder.name == "image" + } + return imageSpecial?.firstOrNull() + } + + companion object { + private const val DRIVE_TYPE_PERSONAL = "personal" + private const val DRIVE_TYPE_PROJECT = "project" + private const val DRIVE_DISABLED = "trashed" + } +} + +data class SpaceOwner( + val user: SpaceUser +) + +data class SpaceQuota( + val remaining: Long?, + val state: String?, + val total: Long, + val used: Long?, +) + +data class SpaceRoot( + val eTag: String, + val id: String, + val permissions: List?, + val webDavUrl: String, + val deleted: SpaceDeleted?, +) + +data class SpaceSpecial( + val eTag: String, + val file: SpaceFile, + val id: String, + val lastModifiedDateTime: String, + val name: String, + val size: Int, + val specialFolder: SpaceSpecialFolder, + val webDavUrl: String +) + +data class SpaceDeleted( + val state: String, +) + +data class SpaceUser( + val id: String +) + +data class SpaceFile( + val mimeType: String +) + +data class SpacePermission( + val grantedTo: List, + val roles: List +) + +data class SpaceGrantedTo( + val user: SpaceUser? +) + +data class SpaceSpecialFolder( + val name: String +) diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetProjectSpacesWithSpecialsForAccountAsStreamUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetProjectSpacesWithSpecialsForAccountAsStreamUseCase.kt new file mode 100644 index 00000000000..92f3db4f63d --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/GetProjectSpacesWithSpecialsForAccountAsStreamUseCase.kt @@ -0,0 +1,37 @@ +/** + * ownCloud Android client application + * + * @author Juan Carlos Garrote Gascón + * + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.spaces.usecases + +import com.owncloud.android.domain.BaseUseCase +import com.owncloud.android.domain.spaces.SpacesRepository +import com.owncloud.android.domain.spaces.model.OCSpace +import kotlinx.coroutines.flow.Flow + +class GetProjectSpacesWithSpecialsForAccountAsStreamUseCase( + private val spacesRepository: SpacesRepository +) : BaseUseCase>, GetProjectSpacesWithSpecialsForAccountAsStreamUseCase.Params>() { + + override fun run(params: Params) = spacesRepository.getProjectSpacesWithSpecialsForAccountAsFlow(params.accountName) + + data class Params( + val accountName: String + ) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/RefreshSpacesFromServerAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/RefreshSpacesFromServerAsyncUseCase.kt new file mode 100644 index 00000000000..87b666d4569 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/spaces/usecases/RefreshSpacesFromServerAsyncUseCase.kt @@ -0,0 +1,34 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2022 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.spaces.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.spaces.SpacesRepository + +class RefreshSpacesFromServerAsyncUseCase( + private val spacesRepository: SpacesRepository +) : BaseUseCaseWithResult() { + + override fun run(params: Params) = + spacesRepository.refreshSpacesForAccount(accountName = params.accountName) + + data class Params( + val accountName: String, + ) +}