Initial QSfera import
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<manifest />
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.qsfera.android.data.authentication.SELECTED_ACCOUNT
|
||||
import eu.qsfera.android.data.providers.SharedPreferencesProvider
|
||||
import eu.qsfera.android.lib.common.ConnectionValidator
|
||||
import eu.qsfera.android.lib.common.QSferaAccount
|
||||
import eu.qsfera.android.lib.common.QSferaClient
|
||||
import eu.qsfera.android.lib.common.SingleSessionManager
|
||||
import eu.qsfera.android.lib.common.authentication.QSferaCredentials
|
||||
import eu.qsfera.android.lib.common.authentication.QSferaCredentialsFactory.getAnonymousCredentials
|
||||
import eu.qsfera.android.lib.resources.appregistry.services.AppRegistryService
|
||||
import eu.qsfera.android.lib.resources.appregistry.services.OCAppRegistryService
|
||||
import eu.qsfera.android.lib.resources.files.services.FileService
|
||||
import eu.qsfera.android.lib.resources.files.services.implementation.OCFileService
|
||||
import eu.qsfera.android.lib.resources.shares.services.ShareService
|
||||
import eu.qsfera.android.lib.resources.shares.services.ShareeService
|
||||
import eu.qsfera.android.lib.resources.shares.services.implementation.OCShareService
|
||||
import eu.qsfera.android.lib.resources.shares.services.implementation.OCShareeService
|
||||
import eu.qsfera.android.lib.resources.spaces.services.OCSpacesService
|
||||
import eu.qsfera.android.lib.resources.spaces.services.SpacesService
|
||||
import eu.qsfera.android.lib.resources.status.services.CapabilityService
|
||||
import eu.qsfera.android.lib.resources.status.services.implementation.OCCapabilityService
|
||||
import eu.qsfera.android.lib.resources.users.services.UserService
|
||||
import eu.qsfera.android.lib.resources.users.services.implementation.OCUserService
|
||||
import timber.log.Timber
|
||||
|
||||
class ClientManager(
|
||||
private val accountManager: AccountManager,
|
||||
private val preferencesProvider: SharedPreferencesProvider,
|
||||
val context: Context,
|
||||
val accountType: String,
|
||||
private val connectionValidator: ConnectionValidator
|
||||
) {
|
||||
// This client will maintain cookies across the whole login process.
|
||||
private var qsferaClient: QSferaClient? = null
|
||||
|
||||
// Cached client to avoid retrieving the client for each service
|
||||
private var qsferaClientForCurrentAccount: QSferaClient? = null
|
||||
|
||||
init {
|
||||
SingleSessionManager.setConnectionValidator(connectionValidator)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a client for the login process.
|
||||
* Helpful to keep the cookies from the status request to the final login and user info retrieval.
|
||||
* For regular uses, use [getClientForAccount]
|
||||
*/
|
||||
fun getClientForAnonymousCredentials(
|
||||
path: String,
|
||||
requiresNewClient: Boolean,
|
||||
qsferaCredentials: QSferaCredentials? = getAnonymousCredentials()
|
||||
): QSferaClient {
|
||||
val safeClient = qsferaClient
|
||||
val pathUri = path.toUri()
|
||||
|
||||
return if (requiresNewClient || safeClient == null || safeClient.baseUri != pathUri) {
|
||||
Timber.d("Creating new client for path: $pathUri. Old client path: ${safeClient?.baseUri}, requiresNewClient: $requiresNewClient")
|
||||
QSferaClient(
|
||||
pathUri,
|
||||
connectionValidator,
|
||||
true,
|
||||
SingleSessionManager.getDefaultSingleton(),
|
||||
context
|
||||
).apply {
|
||||
credentials = qsferaCredentials
|
||||
}.also {
|
||||
qsferaClient = it
|
||||
}
|
||||
} else {
|
||||
Timber.d("Reusing anonymous client for ${safeClient.baseUri}")
|
||||
safeClient.apply {
|
||||
credentials = qsferaCredentials
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getClientForAccount(
|
||||
accountName: String?
|
||||
): QSferaClient {
|
||||
val account: Account? = if (accountName.isNullOrBlank()) {
|
||||
getCurrentAccount()
|
||||
} else {
|
||||
accountManager.getAccountsByType(accountType).firstOrNull { it.name == accountName }
|
||||
}
|
||||
|
||||
val qsferaAccount = QSferaAccount(account, context)
|
||||
return SingleSessionManager.getDefaultSingleton().getClientFor(qsferaAccount, context, connectionValidator).also {
|
||||
qsferaClientForCurrentAccount = it
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCurrentAccount(): Account? {
|
||||
val ocAccounts = accountManager.getAccountsByType(accountType)
|
||||
|
||||
val accountName = preferencesProvider.getString(SELECTED_ACCOUNT, null)
|
||||
|
||||
// account validation: the saved account MUST be in the list of qsfera Accounts known by the AccountManager
|
||||
accountName?.let { selectedAccountName ->
|
||||
ocAccounts.firstOrNull { it.name == selectedAccountName }?.let { return it }
|
||||
}
|
||||
|
||||
// take first account as fallback
|
||||
return ocAccounts.firstOrNull()
|
||||
}
|
||||
|
||||
fun getClientForCoilThumbnails(accountName: String) = getClientForAccount(accountName = accountName)
|
||||
|
||||
fun getUserService(accountName: String? = ""): UserService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCUserService(client = qsferaClient)
|
||||
}
|
||||
|
||||
fun getFileService(accountName: String? = ""): FileService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCFileService(client = qsferaClient)
|
||||
}
|
||||
|
||||
fun getCapabilityService(accountName: String? = ""): CapabilityService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCCapabilityService(client = qsferaClient)
|
||||
}
|
||||
|
||||
fun getShareService(accountName: String? = ""): ShareService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCShareService(client = qsferaClient)
|
||||
}
|
||||
|
||||
fun getShareeService(accountName: String? = ""): ShareeService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCShareeService(client = qsferaClient)
|
||||
}
|
||||
|
||||
fun getSpacesService(accountName: String): SpacesService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCSpacesService(client = qsferaClient)
|
||||
}
|
||||
|
||||
fun getAppRegistryService(accountName: String): AppRegistryService {
|
||||
val qsferaClient = getClientForAccount(accountName)
|
||||
return OCAppRegistryService(client = qsferaClient)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Global executor pools for the whole application.
|
||||
*
|
||||
* Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind
|
||||
* webservice requests).
|
||||
*/
|
||||
open class Executors(
|
||||
private val diskIO: Executor,
|
||||
private val networkIO: Executor,
|
||||
private val mainThread: Executor
|
||||
) {
|
||||
constructor() : this(
|
||||
Executors.newSingleThreadExecutor(),
|
||||
Executors.newFixedThreadPool(3),
|
||||
MainThreadExecutor()
|
||||
)
|
||||
|
||||
fun diskIO(): Executor = diskIO
|
||||
|
||||
fun networkIO(): Executor = networkIO
|
||||
|
||||
fun mainThread(): Executor = mainThread
|
||||
|
||||
private class MainThreadExecutor : Executor {
|
||||
private val mainThreadHandler = Handler(Looper.getMainLooper())
|
||||
override fun execute(command: Runnable) {
|
||||
mainThreadHandler.post(command)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Bartek Przybylski
|
||||
* @author David A. Velasco
|
||||
* @author masensio
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2011 Bartek Przybylski
|
||||
* Copyright (C) 2020 ownCloud GmbH.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
/**
|
||||
* Meta-Class that holds various static field information
|
||||
*/
|
||||
public class ProviderMeta {
|
||||
|
||||
public static final String DB_NAME = "filelist";
|
||||
public static final String NEW_DB_NAME = "qsfera_database";
|
||||
public static final int DB_VERSION = 49;
|
||||
|
||||
private ProviderMeta() {
|
||||
}
|
||||
|
||||
static public class ProviderTableMeta implements BaseColumns {
|
||||
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 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 USER_QUOTAS_TABLE_NAME = "user_quotas";
|
||||
|
||||
public static final String APP_REGISTRY_TABLE_NAME = "app_registry";
|
||||
|
||||
// Columns of ocshares table
|
||||
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_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_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 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_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_TUS_SUPPORT_PREFIX = "tus_support_";
|
||||
public static final String CAPABILITIES_TUS_SUPPORT_VERSION = CAPABILITIES_TUS_SUPPORT_PREFIX + "version";
|
||||
public static final String CAPABILITIES_TUS_SUPPORT_RESUMABLE = CAPABILITIES_TUS_SUPPORT_PREFIX + "resumable";
|
||||
public static final String CAPABILITIES_TUS_SUPPORT_EXTENSION = CAPABILITIES_TUS_SUPPORT_PREFIX + "extension";
|
||||
public static final String CAPABILITIES_TUS_SUPPORT_MAX_CHUNK_SIZE = CAPABILITIES_TUS_SUPPORT_PREFIX + "maxChunkSize";
|
||||
public static final String CAPABILITIES_TUS_SUPPORT_HTTP_METHOD_OVERRIDE = CAPABILITIES_TUS_SUPPORT_PREFIX + "httpMethodOverride";
|
||||
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_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_USER_PROFILE_PICTURE = "sharing_user_profile_picture";
|
||||
public static final String CAPABILITIES_SPACES_PREFIX = "spaces_";
|
||||
public static final String CAPABILITIES_PASSWORD_POLICY_PREFIX = "password_policy_";
|
||||
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 (legacy)
|
||||
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_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_REMOTE_ETAG = "remoteEtag";
|
||||
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_FOR_DATA = "last_sync_date_for_data";
|
||||
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";
|
||||
|
||||
// Columns of list_of_uploads table
|
||||
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_FORCE_OVERWRITE = "force_overwrite";
|
||||
public static final String UPLOAD_LAST_RESULT = "last_result";
|
||||
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";
|
||||
|
||||
// Columns of files table
|
||||
public static final String FILE_OWNER = "owner";
|
||||
public static final String FILE_SPACE_ID = "spaceId";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.room.AutoMigration
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.migration.Migration
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryDao
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity
|
||||
import eu.qsfera.android.data.capabilities.db.OCCapabilityDao
|
||||
import eu.qsfera.android.data.capabilities.db.OCCapabilityEntity
|
||||
import eu.qsfera.android.data.files.db.FileDao
|
||||
import eu.qsfera.android.data.files.db.OCFileEntity
|
||||
import eu.qsfera.android.data.files.db.OCFileSyncEntity
|
||||
import eu.qsfera.android.data.folderbackup.db.FolderBackUpEntity
|
||||
import eu.qsfera.android.data.folderbackup.db.FolderBackupDao
|
||||
import eu.qsfera.android.data.migrations.AutoMigration39To40
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_27_28
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_28_29
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_29_30
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_30_31
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_31_32
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_32_33
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_33_34
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_34_35
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_35_36
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_37_38
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_41_42
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_42_43
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_47_48
|
||||
import eu.qsfera.android.data.migrations.MIGRATION_48_49
|
||||
import eu.qsfera.android.data.sharing.shares.db.OCShareDao
|
||||
import eu.qsfera.android.data.sharing.shares.db.OCShareEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpacesDao
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity
|
||||
import eu.qsfera.android.data.transfers.db.OCTransferEntity
|
||||
import eu.qsfera.android.data.transfers.db.TransferDao
|
||||
import eu.qsfera.android.data.user.db.UserDao
|
||||
import eu.qsfera.android.data.user.db.UserQuotaEntity
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
AppRegistryEntity::class,
|
||||
FolderBackUpEntity::class,
|
||||
OCCapabilityEntity::class,
|
||||
OCFileEntity::class,
|
||||
OCFileSyncEntity::class,
|
||||
OCShareEntity::class,
|
||||
OCTransferEntity::class,
|
||||
SpacesEntity::class,
|
||||
SpaceSpecialEntity::class,
|
||||
UserQuotaEntity::class,
|
||||
],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 36, to = 37),
|
||||
AutoMigration(from = 38, to = 39),
|
||||
AutoMigration(from = 39, to = 40, spec = AutoMigration39To40::class),
|
||||
AutoMigration(from = 40, to = 41),
|
||||
AutoMigration(from = 43, to = 44),
|
||||
AutoMigration(from = 44, to = 45),
|
||||
AutoMigration(from = 45, to = 46),
|
||||
AutoMigration(from = 46, to = 47),
|
||||
],
|
||||
version = ProviderMeta.DB_VERSION,
|
||||
exportSchema = true
|
||||
)
|
||||
abstract class QSferaDatabase : RoomDatabase() {
|
||||
abstract fun appRegistryDao(): AppRegistryDao
|
||||
abstract fun capabilityDao(): OCCapabilityDao
|
||||
abstract fun fileDao(): FileDao
|
||||
abstract fun folderBackUpDao(): FolderBackupDao
|
||||
abstract fun shareDao(): OCShareDao
|
||||
abstract fun spacesDao(): SpacesDao
|
||||
abstract fun transferDao(): TransferDao
|
||||
abstract fun userDao(): UserDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: QSferaDatabase? = null
|
||||
|
||||
fun getDatabase(
|
||||
context: Context
|
||||
): QSferaDatabase =
|
||||
// if the INSTANCE is not null, then return it,
|
||||
// if it is, then create the database
|
||||
INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
QSferaDatabase::class.java,
|
||||
ProviderMeta.NEW_DB_NAME
|
||||
)
|
||||
.addMigrations(
|
||||
MIGRATION_27_28,
|
||||
MIGRATION_28_29,
|
||||
MIGRATION_29_30,
|
||||
MIGRATION_30_31,
|
||||
MIGRATION_31_32,
|
||||
MIGRATION_32_33,
|
||||
MIGRATION_33_34,
|
||||
MIGRATION_34_35,
|
||||
MIGRATION_35_36,
|
||||
MIGRATION_37_38,
|
||||
MIGRATION_41_42,
|
||||
MIGRATION_42_43,
|
||||
MIGRATION_47_48,
|
||||
MIGRATION_48_49)
|
||||
.build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun switchToInMemory(context: Context, vararg migrations: Migration) {
|
||||
INSTANCE = Room.inMemoryDatabaseBuilder(
|
||||
context.applicationContext,
|
||||
QSferaDatabase::class.java
|
||||
).addMigrations(*migrations)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data
|
||||
|
||||
import eu.qsfera.android.domain.exceptions.AccountException
|
||||
import eu.qsfera.android.domain.exceptions.AccountNotFoundException
|
||||
import eu.qsfera.android.domain.exceptions.AccountNotNewException
|
||||
import eu.qsfera.android.domain.exceptions.AccountNotTheSameException
|
||||
import eu.qsfera.android.domain.exceptions.BadOcVersionException
|
||||
import eu.qsfera.android.domain.exceptions.CancelledException
|
||||
import eu.qsfera.android.domain.exceptions.ConflictException
|
||||
import eu.qsfera.android.domain.exceptions.CopyIntoDescendantException
|
||||
import eu.qsfera.android.domain.exceptions.DelayedForWifiException
|
||||
import eu.qsfera.android.domain.exceptions.FileNotFoundException
|
||||
import eu.qsfera.android.domain.exceptions.ForbiddenException
|
||||
import eu.qsfera.android.domain.exceptions.IncorrectAddressException
|
||||
import eu.qsfera.android.domain.exceptions.InstanceNotConfiguredException
|
||||
import eu.qsfera.android.domain.exceptions.InvalidCharacterException
|
||||
import eu.qsfera.android.domain.exceptions.InvalidCharacterInNameException
|
||||
import eu.qsfera.android.domain.exceptions.InvalidLocalFileNameException
|
||||
import eu.qsfera.android.domain.exceptions.InvalidOverwriteException
|
||||
import eu.qsfera.android.domain.exceptions.LocalFileNotFoundException
|
||||
import eu.qsfera.android.domain.exceptions.LocalStorageFullException
|
||||
import eu.qsfera.android.domain.exceptions.LocalStorageNotCopiedException
|
||||
import eu.qsfera.android.domain.exceptions.LocalStorageNotMovedException
|
||||
import eu.qsfera.android.domain.exceptions.LocalStorageNotRemovedException
|
||||
import eu.qsfera.android.domain.exceptions.MoveIntoDescendantException
|
||||
import eu.qsfera.android.domain.exceptions.NetworkErrorException
|
||||
import eu.qsfera.android.domain.exceptions.NoConnectionWithServerException
|
||||
import eu.qsfera.android.domain.exceptions.NoNetworkConnectionException
|
||||
import eu.qsfera.android.domain.exceptions.OAuth2ErrorAccessDeniedException
|
||||
import eu.qsfera.android.domain.exceptions.OAuth2ErrorException
|
||||
import eu.qsfera.android.domain.exceptions.PartialCopyDoneException
|
||||
import eu.qsfera.android.domain.exceptions.PartialMoveDoneException
|
||||
import eu.qsfera.android.domain.exceptions.QuotaExceededException
|
||||
import eu.qsfera.android.domain.exceptions.RedirectToNonSecureException
|
||||
import eu.qsfera.android.domain.exceptions.ResourceLockedException
|
||||
import eu.qsfera.android.domain.exceptions.SSLErrorException
|
||||
import eu.qsfera.android.domain.exceptions.ServerConnectionTimeoutException
|
||||
import eu.qsfera.android.domain.exceptions.ServerNotReachableException
|
||||
import eu.qsfera.android.domain.exceptions.ServerResponseTimeoutException
|
||||
import eu.qsfera.android.domain.exceptions.ServiceUnavailableException
|
||||
import eu.qsfera.android.domain.exceptions.ShareForbiddenException
|
||||
import eu.qsfera.android.domain.exceptions.ShareNotFoundException
|
||||
import eu.qsfera.android.domain.exceptions.ShareWrongParameterException
|
||||
import eu.qsfera.android.domain.exceptions.SpecificForbiddenException
|
||||
import eu.qsfera.android.domain.exceptions.SpecificMethodNotAllowedException
|
||||
import eu.qsfera.android.domain.exceptions.SpecificServiceUnavailableException
|
||||
import eu.qsfera.android.domain.exceptions.SpecificUnsupportedMediaTypeException
|
||||
import eu.qsfera.android.domain.exceptions.SyncConflictException
|
||||
import eu.qsfera.android.domain.exceptions.TooEarlyException
|
||||
import eu.qsfera.android.domain.exceptions.UnauthorizedException
|
||||
import eu.qsfera.android.domain.exceptions.UnhandledHttpCodeException
|
||||
import eu.qsfera.android.domain.exceptions.UnknownErrorException
|
||||
import eu.qsfera.android.domain.exceptions.WrongServerResponseException
|
||||
import eu.qsfera.android.lib.common.network.CertificateCombinedException
|
||||
import eu.qsfera.android.lib.common.operations.RemoteOperationResult
|
||||
import java.net.SocketTimeoutException
|
||||
|
||||
fun <T> executeRemoteOperation(operation: () -> RemoteOperationResult<T>): T {
|
||||
operation.invoke().also {
|
||||
return handleRemoteOperationResult(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> handleRemoteOperationResult(
|
||||
remoteOperationResult: RemoteOperationResult<T>
|
||||
): T {
|
||||
if (remoteOperationResult.isSuccess) {
|
||||
return remoteOperationResult.data
|
||||
}
|
||||
|
||||
when (remoteOperationResult.code) {
|
||||
RemoteOperationResult.ResultCode.WRONG_CONNECTION -> throw NoConnectionWithServerException()
|
||||
RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION -> throw NoNetworkConnectionException()
|
||||
RemoteOperationResult.ResultCode.TIMEOUT ->
|
||||
if (remoteOperationResult.exception is SocketTimeoutException) throw ServerResponseTimeoutException()
|
||||
else throw ServerConnectionTimeoutException()
|
||||
RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE -> throw ServerNotReachableException()
|
||||
RemoteOperationResult.ResultCode.SERVICE_UNAVAILABLE -> throw ServiceUnavailableException()
|
||||
RemoteOperationResult.ResultCode.SSL_RECOVERABLE_PEER_UNVERIFIED -> throw remoteOperationResult.exception as CertificateCombinedException
|
||||
RemoteOperationResult.ResultCode.BAD_OC_VERSION -> throw BadOcVersionException()
|
||||
RemoteOperationResult.ResultCode.INCORRECT_ADDRESS -> throw IncorrectAddressException()
|
||||
RemoteOperationResult.ResultCode.SSL_ERROR -> throw SSLErrorException()
|
||||
RemoteOperationResult.ResultCode.UNAUTHORIZED -> throw UnauthorizedException()
|
||||
RemoteOperationResult.ResultCode.INSTANCE_NOT_CONFIGURED -> throw InstanceNotConfiguredException()
|
||||
RemoteOperationResult.ResultCode.FILE_NOT_FOUND -> throw FileNotFoundException()
|
||||
RemoteOperationResult.ResultCode.OAUTH2_ERROR -> throw OAuth2ErrorException()
|
||||
RemoteOperationResult.ResultCode.OAUTH2_ERROR_ACCESS_DENIED -> throw OAuth2ErrorAccessDeniedException()
|
||||
RemoteOperationResult.ResultCode.ACCOUNT_NOT_NEW -> throw AccountNotNewException()
|
||||
RemoteOperationResult.ResultCode.ACCOUNT_NOT_THE_SAME -> throw AccountNotTheSameException()
|
||||
RemoteOperationResult.ResultCode.OK_REDIRECT_TO_NON_SECURE_CONNECTION -> throw RedirectToNonSecureException()
|
||||
RemoteOperationResult.ResultCode.UNHANDLED_HTTP_CODE -> throw UnhandledHttpCodeException()
|
||||
RemoteOperationResult.ResultCode.UNKNOWN_ERROR -> throw UnknownErrorException()
|
||||
RemoteOperationResult.ResultCode.CANCELLED -> throw CancelledException()
|
||||
RemoteOperationResult.ResultCode.INVALID_LOCAL_FILE_NAME -> throw InvalidLocalFileNameException()
|
||||
RemoteOperationResult.ResultCode.INVALID_OVERWRITE -> throw InvalidOverwriteException()
|
||||
RemoteOperationResult.ResultCode.CONFLICT -> throw ConflictException()
|
||||
RemoteOperationResult.ResultCode.SYNC_CONFLICT -> throw SyncConflictException()
|
||||
RemoteOperationResult.ResultCode.LOCAL_STORAGE_FULL -> throw LocalStorageFullException()
|
||||
RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_MOVED -> throw LocalStorageNotMovedException()
|
||||
RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_COPIED -> throw LocalStorageNotCopiedException()
|
||||
RemoteOperationResult.ResultCode.QUOTA_EXCEEDED -> throw QuotaExceededException()
|
||||
RemoteOperationResult.ResultCode.ACCOUNT_NOT_FOUND -> throw AccountNotFoundException()
|
||||
RemoteOperationResult.ResultCode.ACCOUNT_EXCEPTION -> throw AccountException()
|
||||
RemoteOperationResult.ResultCode.INVALID_CHARACTER_IN_NAME -> throw InvalidCharacterInNameException()
|
||||
RemoteOperationResult.ResultCode.LOCAL_STORAGE_NOT_REMOVED -> throw LocalStorageNotRemovedException()
|
||||
RemoteOperationResult.ResultCode.FORBIDDEN -> throw ForbiddenException()
|
||||
RemoteOperationResult.ResultCode.SPECIFIC_FORBIDDEN -> throw SpecificForbiddenException()
|
||||
RemoteOperationResult.ResultCode.INVALID_MOVE_INTO_DESCENDANT -> throw MoveIntoDescendantException()
|
||||
RemoteOperationResult.ResultCode.INVALID_COPY_INTO_DESCENDANT -> throw CopyIntoDescendantException()
|
||||
RemoteOperationResult.ResultCode.PARTIAL_MOVE_DONE -> throw PartialMoveDoneException()
|
||||
RemoteOperationResult.ResultCode.PARTIAL_COPY_DONE -> throw PartialCopyDoneException()
|
||||
RemoteOperationResult.ResultCode.SHARE_WRONG_PARAMETER -> throw ShareWrongParameterException()
|
||||
RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE -> throw WrongServerResponseException()
|
||||
RemoteOperationResult.ResultCode.INVALID_CHARACTER_DETECT_IN_SERVER -> throw InvalidCharacterException()
|
||||
RemoteOperationResult.ResultCode.DELAYED_FOR_WIFI -> throw DelayedForWifiException()
|
||||
RemoteOperationResult.ResultCode.LOCAL_FILE_NOT_FOUND -> throw LocalFileNotFoundException()
|
||||
RemoteOperationResult.ResultCode.SPECIFIC_SERVICE_UNAVAILABLE -> throw SpecificServiceUnavailableException(remoteOperationResult.httpPhrase)
|
||||
RemoteOperationResult.ResultCode.SPECIFIC_UNSUPPORTED_MEDIA_TYPE -> throw SpecificUnsupportedMediaTypeException()
|
||||
RemoteOperationResult.ResultCode.SPECIFIC_METHOD_NOT_ALLOWED -> throw SpecificMethodNotAllowedException(remoteOperationResult.httpPhrase)
|
||||
RemoteOperationResult.ResultCode.SHARE_NOT_FOUND -> throw ShareNotFoundException(remoteOperationResult.httpPhrase)
|
||||
RemoteOperationResult.ResultCode.SHARE_FORBIDDEN -> throw ShareForbiddenException(remoteOperationResult.httpPhrase)
|
||||
RemoteOperationResult.ResultCode.TOO_EARLY -> throw TooEarlyException()
|
||||
RemoteOperationResult.ResultCode.NETWORK_ERROR -> throw NetworkErrorException()
|
||||
RemoteOperationResult.ResultCode.RESOURCE_LOCKED -> throw ResourceLockedException()
|
||||
else -> throw Exception("An unknown error has occurred")
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.appregistry.datasources
|
||||
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistry
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalAppRegistryDataSource {
|
||||
fun getAppRegistryForMimeTypeAsStream(
|
||||
accountName: String,
|
||||
mimeType: String,
|
||||
): Flow<AppRegistryMimeType?>
|
||||
|
||||
fun getAppRegistryWhichAllowCreation(
|
||||
accountName: String,
|
||||
): Flow<List<AppRegistryMimeType>>
|
||||
|
||||
fun saveAppRegistryForAccount(
|
||||
appRegistry: AppRegistry
|
||||
)
|
||||
|
||||
fun deleteAppRegistryForAccount(
|
||||
accountName: String,
|
||||
)
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.appregistry.datasources
|
||||
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistry
|
||||
|
||||
interface RemoteAppRegistryDataSource {
|
||||
fun getAppRegistryForAccount(
|
||||
accountName: String,
|
||||
appUrl: String?,
|
||||
): AppRegistry
|
||||
|
||||
fun getUrlToOpenInWeb(
|
||||
accountName: String,
|
||||
openWebEndpoint: String,
|
||||
fileId: String,
|
||||
appName: String,
|
||||
): String
|
||||
|
||||
fun createFileWithAppProvider(
|
||||
accountName: String,
|
||||
createFileWithAppProviderEndpoint: String,
|
||||
parentContainerId: String,
|
||||
filename: String,
|
||||
): String
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.appregistry.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.appregistry.datasources.LocalAppRegistryDataSource
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryDao
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistry
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistryProvider
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class OCLocalAppRegistryDataSource(
|
||||
private val appRegistryDao: AppRegistryDao,
|
||||
) : LocalAppRegistryDataSource {
|
||||
override fun getAppRegistryForMimeTypeAsStream(accountName: String, mimeType: String): Flow<AppRegistryMimeType?> =
|
||||
appRegistryDao.getAppRegistryForMimeType(accountName, mimeType).map { it?.toModel() }
|
||||
|
||||
override fun getAppRegistryWhichAllowCreation(accountName: String): Flow<List<AppRegistryMimeType>> =
|
||||
appRegistryDao.getAppRegistryWhichAllowCreation(accountName).map { listAppRegistryEntities ->
|
||||
listAppRegistryEntities.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun saveAppRegistryForAccount(appRegistry: AppRegistry) {
|
||||
val appRegistryEntitiesToInsert = mutableListOf<AppRegistryEntity>()
|
||||
|
||||
val newAppRegistryEntities = appRegistry.toEntities(appRegistry.accountName)
|
||||
newAppRegistryEntities.forEach { appRegistryEntity ->
|
||||
appRegistryEntitiesToInsert.add(appRegistryEntity)
|
||||
}
|
||||
|
||||
appRegistryDao.deleteAppRegistryForAccount(appRegistry.accountName)
|
||||
appRegistryDao.upsertAppRegistries(appRegistryEntitiesToInsert)
|
||||
}
|
||||
|
||||
override fun deleteAppRegistryForAccount(accountName: String) {
|
||||
appRegistryDao.deleteAppRegistryForAccount(accountName)
|
||||
}
|
||||
|
||||
private fun AppRegistry.toEntities(accountName: String): List<AppRegistryEntity> =
|
||||
mimetypes.map { appRegistryMimeTypes ->
|
||||
AppRegistryEntity(
|
||||
accountName = accountName,
|
||||
mimeType = appRegistryMimeTypes.mimeType,
|
||||
ext = appRegistryMimeTypes.ext,
|
||||
appProviders = appRegistryMimeTypes.appProviders.toJsonString(),
|
||||
name = appRegistryMimeTypes.name,
|
||||
icon = appRegistryMimeTypes.icon,
|
||||
description = appRegistryMimeTypes.description,
|
||||
allowCreation = appRegistryMimeTypes.allowCreation,
|
||||
defaultApplication = appRegistryMimeTypes.defaultApplication,
|
||||
)
|
||||
}
|
||||
|
||||
private fun AppRegistryEntity.toModel(): AppRegistryMimeType =
|
||||
AppRegistryMimeType(
|
||||
mimeType = mimeType,
|
||||
ext = ext,
|
||||
appProviders = appProviders.toAppRegistryProvider(),
|
||||
name = name,
|
||||
icon = icon,
|
||||
description = description,
|
||||
allowCreation = allowCreation,
|
||||
defaultApplication = defaultApplication,
|
||||
)
|
||||
|
||||
private fun List<AppRegistryProvider>.toJsonString(): String {
|
||||
val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||
val type: Type = Types.newParameterizedType(List::class.java, AppRegistryProvider::class.java)
|
||||
|
||||
val jsonAdapter: JsonAdapter<List<AppRegistryProvider>> = moshi.adapter(type)
|
||||
|
||||
return jsonAdapter.toJson(this)
|
||||
}
|
||||
|
||||
private fun String.toAppRegistryProvider(): List<AppRegistryProvider> {
|
||||
val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||
val type: Type = Types.newParameterizedType(List::class.java, AppRegistryProvider::class.java)
|
||||
|
||||
val jsonAdapter: JsonAdapter<List<AppRegistryProvider>> = moshi.adapter(type)
|
||||
|
||||
return jsonAdapter.fromJson(this) ?: emptyList()
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.appregistry.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.appregistry.datasources.RemoteAppRegistryDataSource
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistry
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistryProvider
|
||||
import eu.qsfera.android.lib.resources.appregistry.responses.AppRegistryResponse
|
||||
|
||||
class OCRemoteAppRegistryDataSource(
|
||||
private val clientManager: ClientManager
|
||||
) : RemoteAppRegistryDataSource {
|
||||
override fun getAppRegistryForAccount(accountName: String, appUrl: String?): AppRegistry =
|
||||
executeRemoteOperation {
|
||||
clientManager.getAppRegistryService(accountName).getAppRegistry(appUrl)
|
||||
}.toModel(accountName)
|
||||
|
||||
override fun getUrlToOpenInWeb(
|
||||
accountName: String,
|
||||
openWebEndpoint: String,
|
||||
fileId: String,
|
||||
appName: String,
|
||||
): String =
|
||||
executeRemoteOperation {
|
||||
clientManager.getAppRegistryService(accountName).getUrlToOpenInWeb(
|
||||
openWebEndpoint = openWebEndpoint,
|
||||
fileId = fileId,
|
||||
appName = appName,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createFileWithAppProvider(
|
||||
accountName: String,
|
||||
createFileWithAppProviderEndpoint: String,
|
||||
parentContainerId: String,
|
||||
filename: String,
|
||||
): String =
|
||||
executeRemoteOperation {
|
||||
clientManager.getAppRegistryService(accountName).createFileWithAppProvider(
|
||||
createFileWithAppProviderEndpoint = createFileWithAppProviderEndpoint,
|
||||
parentContainerId = parentContainerId,
|
||||
filename = filename,
|
||||
)
|
||||
}
|
||||
|
||||
private fun AppRegistryResponse.toModel(accountName: String) =
|
||||
AppRegistry(
|
||||
accountName = accountName,
|
||||
mimetypes = value.map { appRegistryMimeTypeResponse ->
|
||||
AppRegistryMimeType(
|
||||
mimeType = appRegistryMimeTypeResponse.mimeType,
|
||||
ext = appRegistryMimeTypeResponse.ext,
|
||||
appProviders = appRegistryMimeTypeResponse.appProviders.map { appRegistryProviderResponse ->
|
||||
AppRegistryProvider(
|
||||
name = appRegistryProviderResponse.name,
|
||||
productName = appRegistryProviderResponse.productName,
|
||||
icon = appRegistryProviderResponse.icon
|
||||
)
|
||||
},
|
||||
name = appRegistryMimeTypeResponse.name,
|
||||
icon = appRegistryMimeTypeResponse.icon,
|
||||
description = appRegistryMimeTypeResponse.description,
|
||||
allowCreation = appRegistryMimeTypeResponse.allowCreation,
|
||||
defaultApplication = appRegistryMimeTypeResponse.defaultApplication,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.appregistry.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_ALLOW_CREATION
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_MIME_TYPE
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface AppRegistryDao {
|
||||
@Upsert
|
||||
fun upsertAppRegistries(appRegistryEntities: List<AppRegistryEntity>)
|
||||
|
||||
@Query(SELECT_APP_REGISTRY_FOR_ACCOUNT_AND_MIME_TYPE)
|
||||
fun getAppRegistryForMimeType(
|
||||
accountName: String,
|
||||
mimeType: String,
|
||||
): Flow<AppRegistryEntity?>
|
||||
|
||||
@Query(SELECT_APP_REGISTRY_ALLOW_CREATION_FOR_ACCOUNT)
|
||||
fun getAppRegistryWhichAllowCreation(
|
||||
accountName: String,
|
||||
): Flow<List<AppRegistryEntity>>
|
||||
|
||||
@Query(DELETE_APP_REGISTRY_FOR_ACCOUNT)
|
||||
fun deleteAppRegistryForAccount(accountName: String)
|
||||
|
||||
companion object {
|
||||
private const val SELECT_APP_REGISTRY_FOR_ACCOUNT_AND_MIME_TYPE = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME}
|
||||
WHERE $APP_REGISTRY_ACCOUNT_NAME = :accountName AND $APP_REGISTRY_MIME_TYPE = :mimeType
|
||||
"""
|
||||
|
||||
private const val SELECT_APP_REGISTRY_ALLOW_CREATION_FOR_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME}
|
||||
WHERE $APP_REGISTRY_ACCOUNT_NAME = :accountName AND $APP_REGISTRY_ALLOW_CREATION = 1
|
||||
"""
|
||||
|
||||
private const val DELETE_APP_REGISTRY_FOR_ACCOUNT = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME}
|
||||
WHERE $APP_REGISTRY_ACCOUNT_NAME = :accountName
|
||||
"""
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.appregistry.db
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.appregistry.db.AppRegistryEntity.Companion.APP_REGISTRY_MIME_TYPE
|
||||
|
||||
@Entity(
|
||||
tableName = ProviderMeta.ProviderTableMeta.APP_REGISTRY_TABLE_NAME,
|
||||
primaryKeys = [APP_REGISTRY_ACCOUNT_NAME, APP_REGISTRY_MIME_TYPE]
|
||||
)
|
||||
data class AppRegistryEntity(
|
||||
@ColumnInfo(name = APP_REGISTRY_ACCOUNT_NAME)
|
||||
val accountName: String,
|
||||
@ColumnInfo(name = APP_REGISTRY_MIME_TYPE)
|
||||
val mimeType: String,
|
||||
val ext: String? = null,
|
||||
@ColumnInfo(name = APP_REGISTRY_APP_PROVIDERS)
|
||||
val appProviders: String,
|
||||
val name: String? = null,
|
||||
val icon: String? = null,
|
||||
val description: String? = null,
|
||||
@ColumnInfo(name = APP_REGISTRY_ALLOW_CREATION)
|
||||
val allowCreation: Boolean? = null,
|
||||
@ColumnInfo(name = APP_REGISTRY_DEFAULT_APPLICATION)
|
||||
val defaultApplication: String? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
const val APP_REGISTRY_MIME_TYPES = "mime_types"
|
||||
const val APP_REGISTRY_ACCOUNT_NAME = "account_name"
|
||||
const val APP_REGISTRY_MIME_TYPE = "mime_type"
|
||||
const val APP_REGISTRY_APP_PROVIDERS = "app_providers"
|
||||
const val APP_REGISTRY_ALLOW_CREATION = "allow_creation"
|
||||
const val APP_REGISTRY_DEFAULT_APPLICATION = "default_application"
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.appregistry.repository
|
||||
|
||||
import eu.qsfera.android.data.appregistry.datasources.LocalAppRegistryDataSource
|
||||
import eu.qsfera.android.data.appregistry.datasources.RemoteAppRegistryDataSource
|
||||
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
|
||||
import eu.qsfera.android.domain.appregistry.AppRegistryRepository
|
||||
import eu.qsfera.android.domain.appregistry.model.AppRegistryMimeType
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class OCAppRegistryRepository(
|
||||
private val localAppRegistryDataSource: LocalAppRegistryDataSource,
|
||||
private val remoteAppRegistryDataSource: RemoteAppRegistryDataSource,
|
||||
private val localCapabilitiesDataSource: LocalCapabilitiesDataSource,
|
||||
) : AppRegistryRepository {
|
||||
override fun refreshAppRegistryForAccount(accountName: String) {
|
||||
val capabilities = localCapabilitiesDataSource.getCapabilitiesForAccount(accountName)
|
||||
val appUrl = capabilities?.filesAppProviders?.appsUrl?.substring(1)
|
||||
remoteAppRegistryDataSource.getAppRegistryForAccount(accountName, appUrl).also {
|
||||
localAppRegistryDataSource.saveAppRegistryForAccount(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAppRegistryForMimeTypeAsStream(accountName: String, mimeType: String): Flow<AppRegistryMimeType?> =
|
||||
localAppRegistryDataSource.getAppRegistryForMimeTypeAsStream(accountName, mimeType)
|
||||
|
||||
override fun getAppRegistryWhichAllowCreation(accountName: String): Flow<List<AppRegistryMimeType>> =
|
||||
localAppRegistryDataSource.getAppRegistryWhichAllowCreation(accountName)
|
||||
|
||||
override fun getUrlToOpenInWeb(accountName: String, openWebEndpoint: String, fileId: String, appName: String): String =
|
||||
remoteAppRegistryDataSource.getUrlToOpenInWeb(
|
||||
accountName = accountName,
|
||||
openWebEndpoint = openWebEndpoint,
|
||||
fileId = fileId,
|
||||
appName = appName,
|
||||
)
|
||||
|
||||
override fun createFileWithAppProvider(
|
||||
accountName: String,
|
||||
createFileWithAppProviderEndpoint: String,
|
||||
parentContainerId: String,
|
||||
filename: String,
|
||||
): String =
|
||||
remoteAppRegistryDataSource.createFileWithAppProvider(
|
||||
accountName = accountName,
|
||||
createFileWithAppProviderEndpoint = createFileWithAppProviderEndpoint,
|
||||
parentContainerId = parentContainerId,
|
||||
filename = filename,
|
||||
)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.authentication
|
||||
|
||||
const val SELECTED_ACCOUNT = "select_oc_account"
|
||||
|
||||
/**
|
||||
* OAuth2 user id
|
||||
*/
|
||||
const val KEY_USER_ID = "user_id"
|
||||
|
||||
/**
|
||||
* OAuth2 refresh token
|
||||
*/
|
||||
const val KEY_OAUTH2_REFRESH_TOKEN = "oc_oauth2_refresh_token"
|
||||
|
||||
/**
|
||||
* OAuth2 scope
|
||||
*/
|
||||
const val KEY_OAUTH2_SCOPE = "oc_oauth2_scope"
|
||||
|
||||
/**
|
||||
* Features allowed for the account
|
||||
*/
|
||||
const val KEY_FEATURE_ALLOWED = "KEY_FEATURE_ALLOWED"
|
||||
const val KEY_FEATURE_SPACES = "KEY_FEATURE_SPACES"
|
||||
|
||||
/**
|
||||
* OIDC Client Registration
|
||||
*/
|
||||
/**
|
||||
* Preferred username from OIDC id_token, used for login_hint
|
||||
*/
|
||||
const val KEY_PREFERRED_USERNAME = "oc_preferred_username"
|
||||
|
||||
/**
|
||||
* OIDC issuer URL from webfinger, used for token refresh OIDC discovery
|
||||
*/
|
||||
const val KEY_OIDC_ISSUER = "oc_oidc_issuer"
|
||||
|
||||
const val KEY_CLIENT_REGISTRATION_CLIENT_ID = "client_id"
|
||||
const val KEY_CLIENT_REGISTRATION_CLIENT_SECRET = "client_secret"
|
||||
const val KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE = "client_secret_expires_at"
|
||||
|
||||
/** Query parameters to retrieve the authorization code. More info: https://tools.ietf.org/html/rfc6749#section-4.1.1 */
|
||||
const val QUERY_PARAMETER_REDIRECT_URI = "redirect_uri"
|
||||
const val QUERY_PARAMETER_CLIENT_ID = "client_id"
|
||||
const val QUERY_PARAMETER_RESPONSE_TYPE = "response_type"
|
||||
const val QUERY_PARAMETER_SCOPE = "scope"
|
||||
const val QUERY_PARAMETER_PROMPT = "prompt"
|
||||
const val QUERY_PARAMETER_CODE_CHALLENGE = "code_challenge"
|
||||
const val QUERY_PARAMETER_CODE_CHALLENGE_METHOD = "code_challenge_method"
|
||||
const val QUERY_PARAMETER_STATE = "state"
|
||||
const val QUERY_PARAMETER_USER = "user"
|
||||
const val QUERY_PARAMETER_LOGIN_HINT = "login_hint"
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.authentication.datasources
|
||||
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
|
||||
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
|
||||
interface LocalAuthenticationDataSource {
|
||||
fun addBasicAccount(
|
||||
userName: String,
|
||||
lastPermanentLocation: String?,
|
||||
password: String,
|
||||
serverInfo: ServerInfo,
|
||||
userInfo: UserInfo,
|
||||
updateAccountWithUsername: String?
|
||||
): String
|
||||
|
||||
fun addOAuthAccount(
|
||||
userName: String,
|
||||
lastPermanentLocation: String?,
|
||||
authTokenType: String,
|
||||
accessToken: String,
|
||||
serverInfo: ServerInfo,
|
||||
userInfo: UserInfo,
|
||||
refreshToken: String,
|
||||
scope: String?,
|
||||
updateAccountWithUsername: String?,
|
||||
clientRegistrationInfo: ClientRegistrationInfo?
|
||||
): String
|
||||
|
||||
fun supportsOAuth2(accountName: String): Boolean
|
||||
|
||||
fun getBaseUrl(accountName: String): String
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.authentication.datasources
|
||||
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
|
||||
interface RemoteAuthenticationDataSource {
|
||||
// Returns a Pair with UserInfo and last permanent redirection
|
||||
fun loginBasic(serverPath: String, username: String, password: String): Pair<UserInfo, String?>
|
||||
fun loginOAuth(serverPath: String, username: String, accessToken: String): Pair<UserInfo, String?>
|
||||
}
|
||||
+258
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.authentication.datasources.implementation
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE
|
||||
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_ID
|
||||
import eu.qsfera.android.data.authentication.KEY_CLIENT_REGISTRATION_CLIENT_SECRET
|
||||
import eu.qsfera.android.data.authentication.KEY_FEATURE_ALLOWED
|
||||
import eu.qsfera.android.data.authentication.KEY_FEATURE_SPACES
|
||||
import eu.qsfera.android.data.authentication.KEY_OAUTH2_REFRESH_TOKEN
|
||||
import eu.qsfera.android.data.authentication.KEY_OAUTH2_SCOPE
|
||||
import eu.qsfera.android.data.authentication.SELECTED_ACCOUNT
|
||||
import eu.qsfera.android.data.authentication.datasources.LocalAuthenticationDataSource
|
||||
import eu.qsfera.android.data.providers.SharedPreferencesProvider
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
|
||||
import eu.qsfera.android.domain.exceptions.AccountNotFoundException
|
||||
import eu.qsfera.android.domain.exceptions.AccountNotNewException
|
||||
import eu.qsfera.android.domain.exceptions.AccountNotTheSameException
|
||||
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
import eu.qsfera.android.lib.common.SingleSessionManager
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.ACCOUNT_VERSION
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_DISPLAY_NAME
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_ID
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.KEY_SUPPORTS_OAUTH2
|
||||
import eu.qsfera.android.lib.common.accounts.AccountUtils.Constants.OAUTH_SUPPORTED_TRUE
|
||||
import eu.qsfera.android.lib.common.authentication.QSferaBasicCredentials
|
||||
import eu.qsfera.android.lib.common.authentication.QSferaBearerCredentials
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
class OCLocalAuthenticationDataSource(
|
||||
private val context: Context,
|
||||
private val accountManager: AccountManager,
|
||||
private val preferencesProvider: SharedPreferencesProvider,
|
||||
private val accountType: String
|
||||
) : LocalAuthenticationDataSource {
|
||||
|
||||
override fun addBasicAccount(
|
||||
userName: String,
|
||||
lastPermanentLocation: String?,
|
||||
password: String,
|
||||
serverInfo: ServerInfo,
|
||||
userInfo: UserInfo,
|
||||
updateAccountWithUsername: String?
|
||||
): String =
|
||||
addAccount(
|
||||
lastPermanentLocation = lastPermanentLocation,
|
||||
serverInfo = serverInfo,
|
||||
userName = userName,
|
||||
password = password,
|
||||
updateAccountWithUsername = updateAccountWithUsername
|
||||
).also { account ->
|
||||
updateAccountWithUsername?.let {
|
||||
accountManager.setPassword(account, password)
|
||||
SingleSessionManager.getDefaultSingleton().refreshCredentialsForAccount(
|
||||
account.name, QSferaBasicCredentials(userName, password)
|
||||
)
|
||||
}
|
||||
updateUserAndServerInfo(account, serverInfo, userInfo)
|
||||
}.name
|
||||
|
||||
override fun addOAuthAccount(
|
||||
userName: String,
|
||||
lastPermanentLocation: String?,
|
||||
authTokenType: String,
|
||||
accessToken: String,
|
||||
serverInfo: ServerInfo,
|
||||
userInfo: UserInfo,
|
||||
refreshToken: String,
|
||||
scope: String?,
|
||||
updateAccountWithUsername: String?,
|
||||
clientRegistrationInfo: ClientRegistrationInfo?
|
||||
): String =
|
||||
addAccount(
|
||||
lastPermanentLocation = lastPermanentLocation,
|
||||
serverInfo = serverInfo,
|
||||
userName = userName,
|
||||
updateAccountWithUsername = updateAccountWithUsername
|
||||
).also {
|
||||
updateUserAndServerInfo(it, serverInfo, userInfo)
|
||||
|
||||
accountManager.setAuthToken(it, authTokenType, accessToken)
|
||||
|
||||
updateAccountWithUsername?.let { userName ->
|
||||
SingleSessionManager.getDefaultSingleton().refreshCredentialsForAccount(
|
||||
it.name, QSferaBearerCredentials(userName, accessToken)
|
||||
)
|
||||
}
|
||||
|
||||
clientRegistrationInfo?.let { clientRegistrationInfo ->
|
||||
accountManager.apply {
|
||||
setUserData(it, KEY_CLIENT_REGISTRATION_CLIENT_ID, clientRegistrationInfo.clientId)
|
||||
setUserData(it, KEY_CLIENT_REGISTRATION_CLIENT_SECRET, clientRegistrationInfo.clientSecret)
|
||||
setUserData(it, KEY_CLIENT_REGISTRATION_CLIENT_EXPIRATION_DATE, clientRegistrationInfo.clientSecretExpiration.toString())
|
||||
}
|
||||
}
|
||||
|
||||
accountManager.setUserData(it, KEY_SUPPORTS_OAUTH2, OAUTH_SUPPORTED_TRUE)
|
||||
accountManager.setUserData(it, KEY_OAUTH2_REFRESH_TOKEN, refreshToken)
|
||||
scope?.run {
|
||||
accountManager.setUserData(it, KEY_OAUTH2_SCOPE, this)
|
||||
}
|
||||
}.name
|
||||
|
||||
/**
|
||||
* Add new account to account manager, shared preferences and returns account
|
||||
*/
|
||||
private fun addAccount(
|
||||
lastPermanentLocation: String?,
|
||||
serverInfo: ServerInfo,
|
||||
userName: String,
|
||||
password: String = "",
|
||||
updateAccountWithUsername: String?
|
||||
): Account {
|
||||
|
||||
lastPermanentLocation?.let {
|
||||
serverInfo.baseUrl = it
|
||||
}
|
||||
|
||||
val uri = Uri.parse(serverInfo.baseUrl)
|
||||
|
||||
val accountName = AccountUtils.buildAccountName(uri, userName)
|
||||
|
||||
// Check if the entered user matches the user of the account to update
|
||||
if (!updateAccountWithUsername.isNullOrBlank() && accountName != updateAccountWithUsername) {
|
||||
throw AccountNotTheSameException()
|
||||
}
|
||||
|
||||
val account = getAccountIfExists(accountName)
|
||||
if (account != null) {
|
||||
if (updateAccountWithUsername.isNullOrBlank()) {
|
||||
Timber.d("The account already exists")
|
||||
throw AccountNotNewException()
|
||||
} else {
|
||||
return account // Account won't be null, since we've already checked it exists
|
||||
}
|
||||
} else {
|
||||
val newAccount = Account(accountName, accountType)
|
||||
|
||||
// with external authorizations, the password is never input in the app
|
||||
accountManager.addAccountExplicitly(newAccount, password, null)
|
||||
|
||||
// Only fresh accounts will support spaces
|
||||
accountManager.setUserData(newAccount, KEY_FEATURE_SPACES, KEY_FEATURE_ALLOWED)
|
||||
|
||||
/// add the new account as default in preferences, if there is none already
|
||||
val defaultAccount: Account? = getCurrentAccount()
|
||||
if (defaultAccount == null) {
|
||||
preferencesProvider.putString(SELECTED_ACCOUNT, accountName)
|
||||
}
|
||||
|
||||
return newAccount
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUserAndServerInfo(
|
||||
newAccount: Account,
|
||||
serverInfo: ServerInfo,
|
||||
userInfo: UserInfo
|
||||
) {
|
||||
// include account version with the new account
|
||||
accountManager.setUserData(
|
||||
newAccount,
|
||||
KEY_OC_ACCOUNT_VERSION,
|
||||
ACCOUNT_VERSION.toString()
|
||||
)
|
||||
|
||||
accountManager.setUserData(
|
||||
newAccount, KEY_OC_BASE_URL, serverInfo.baseUrl
|
||||
)
|
||||
|
||||
accountManager.setUserData(
|
||||
newAccount, KEY_DISPLAY_NAME, userInfo.displayName
|
||||
)
|
||||
|
||||
accountManager.setUserData(
|
||||
newAccount, KEY_ID, userInfo.id
|
||||
)
|
||||
}
|
||||
|
||||
private fun getAccountIfExists(accountName: String): Account? {
|
||||
val ocAccounts: Array<Account> = getAccounts()
|
||||
|
||||
var lastAtPos = accountName.lastIndexOf("@")
|
||||
val hostAndPort = accountName.substring(lastAtPos + 1)
|
||||
val username = accountName.substring(0, lastAtPos)
|
||||
var otherHostAndPort: String
|
||||
var otherUsername: String
|
||||
val currentLocale: Locale = context.resources.configuration.locale
|
||||
for (otherAccount in ocAccounts) {
|
||||
lastAtPos = otherAccount.name.lastIndexOf("@")
|
||||
otherHostAndPort = otherAccount.name.substring(lastAtPos + 1)
|
||||
otherUsername = otherAccount.name.substring(0, lastAtPos)
|
||||
if (otherHostAndPort == hostAndPort && otherUsername.lowercase(currentLocale) == username.lowercase(currentLocale)) {
|
||||
return otherAccount
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getAccounts(): Array<Account> =
|
||||
accountManager.getAccountsByType(accountType)
|
||||
|
||||
private fun getCurrentAccount(): Account? {
|
||||
val ocAccounts = getAccounts()
|
||||
var defaultAccount: Account? = null
|
||||
|
||||
val accountName = preferencesProvider.getString(SELECTED_ACCOUNT, null)
|
||||
|
||||
// account validation: the saved account MUST be in the list of qsfera Accounts known by the AccountManager
|
||||
if (accountName != null) {
|
||||
for (account in ocAccounts) {
|
||||
if (account.name == accountName) {
|
||||
defaultAccount = account
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultAccount
|
||||
}
|
||||
|
||||
override fun supportsOAuth2(accountName: String): Boolean {
|
||||
val account = getAccountIfExists(accountName) ?: throw AccountNotFoundException()
|
||||
return accountManager.getUserData(account, KEY_SUPPORTS_OAUTH2) == OAUTH_SUPPORTED_TRUE
|
||||
}
|
||||
|
||||
override fun getBaseUrl(accountName: String): String {
|
||||
val account = getAccountIfExists(accountName) ?: throw AccountNotFoundException()
|
||||
return accountManager.getUserData(account, KEY_OC_BASE_URL)
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.authentication.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.authentication.datasources.RemoteAuthenticationDataSource
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.user.datasources.implementation.toDomain
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
import eu.qsfera.android.lib.common.QSferaClient
|
||||
import eu.qsfera.android.lib.common.QSferaClient.WEBDAV_FILES_PATH_4_0
|
||||
import eu.qsfera.android.lib.common.authentication.QSferaCredentials
|
||||
import eu.qsfera.android.lib.common.authentication.QSferaCredentialsFactory
|
||||
import eu.qsfera.android.lib.resources.files.GetBaseUrlRemoteOperation
|
||||
import eu.qsfera.android.lib.resources.users.GetRemoteUserInfoOperation
|
||||
|
||||
class OCRemoteAuthenticationDataSource(
|
||||
private val clientManager: ClientManager
|
||||
) : RemoteAuthenticationDataSource {
|
||||
override fun loginBasic(serverPath: String, username: String, password: String): Pair<UserInfo, String?> =
|
||||
login(QSferaCredentialsFactory.newBasicCredentials(username, password), serverPath)
|
||||
|
||||
override fun loginOAuth(serverPath: String, username: String, accessToken: String): Pair<UserInfo, String?> =
|
||||
login(QSferaCredentialsFactory.newBearerCredentials(username, accessToken), serverPath)
|
||||
|
||||
private fun login(qsferaCredentials: QSferaCredentials, serverPath: String): Pair<UserInfo, String?> {
|
||||
|
||||
val client: QSferaClient =
|
||||
clientManager.getClientForAnonymousCredentials(
|
||||
path = serverPath,
|
||||
requiresNewClient = false
|
||||
).apply { credentials = qsferaCredentials }
|
||||
|
||||
val getBaseUrlRemoteOperation = GetBaseUrlRemoteOperation()
|
||||
val rawBaseUrl = executeRemoteOperation { getBaseUrlRemoteOperation.execute(client) }
|
||||
|
||||
val userBaseUri = rawBaseUrl?.replace(WEBDAV_FILES_PATH_4_0, "")
|
||||
?: client.baseUri.toString()
|
||||
|
||||
// Get user info. It is needed to save the account into the account manager
|
||||
lateinit var userInfo: UserInfo
|
||||
|
||||
executeRemoteOperation {
|
||||
GetRemoteUserInfoOperation().execute(client)
|
||||
}.let { userInfo = it.toDomain() }
|
||||
|
||||
return Pair(userInfo, userBaseUri)
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.authentication.repository
|
||||
|
||||
import eu.qsfera.android.data.authentication.datasources.LocalAuthenticationDataSource
|
||||
import eu.qsfera.android.data.authentication.datasources.RemoteAuthenticationDataSource
|
||||
import eu.qsfera.android.domain.authentication.AuthenticationRepository
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
|
||||
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
|
||||
class OCAuthenticationRepository(
|
||||
private val localAuthenticationDataSource: LocalAuthenticationDataSource,
|
||||
private val remoteAuthenticationDataSource: RemoteAuthenticationDataSource
|
||||
) : AuthenticationRepository {
|
||||
override fun loginBasic(
|
||||
serverInfo: ServerInfo,
|
||||
username: String,
|
||||
password: String,
|
||||
updateAccountWithUsername: String?
|
||||
): String {
|
||||
val userInfoAndRedirectionPath: Pair<UserInfo, String?> =
|
||||
remoteAuthenticationDataSource.loginBasic(
|
||||
serverPath = serverInfo.baseUrl,
|
||||
username = username,
|
||||
password = password
|
||||
)
|
||||
|
||||
return localAuthenticationDataSource.addBasicAccount(
|
||||
userName = username,
|
||||
lastPermanentLocation = userInfoAndRedirectionPath.second,
|
||||
password = password,
|
||||
serverInfo = serverInfo,
|
||||
userInfo = userInfoAndRedirectionPath.first,
|
||||
updateAccountWithUsername = updateAccountWithUsername
|
||||
)
|
||||
}
|
||||
|
||||
override fun loginOAuth(
|
||||
serverInfo: ServerInfo,
|
||||
username: String,
|
||||
authTokenType: String,
|
||||
accessToken: String,
|
||||
refreshToken: String,
|
||||
scope: String?,
|
||||
updateAccountWithUsername: String?,
|
||||
clientRegistrationInfo: ClientRegistrationInfo?
|
||||
): String {
|
||||
val userInfoAndRedirectionPath: Pair<UserInfo, String?> =
|
||||
remoteAuthenticationDataSource.loginOAuth(
|
||||
serverPath = serverInfo.baseUrl,
|
||||
username = username,
|
||||
accessToken = accessToken
|
||||
)
|
||||
|
||||
return localAuthenticationDataSource.addOAuthAccount(
|
||||
userName = username.ifBlank { userInfoAndRedirectionPath.first.id },
|
||||
lastPermanentLocation = userInfoAndRedirectionPath.second,
|
||||
authTokenType = authTokenType,
|
||||
accessToken = accessToken,
|
||||
serverInfo = serverInfo,
|
||||
userInfo = userInfoAndRedirectionPath.first,
|
||||
refreshToken = refreshToken,
|
||||
scope = scope,
|
||||
updateAccountWithUsername = updateAccountWithUsername,
|
||||
clientRegistrationInfo = clientRegistrationInfo
|
||||
)
|
||||
}
|
||||
|
||||
override fun supportsOAuth2UseCase(accountName: String): Boolean =
|
||||
localAuthenticationDataSource.supportsOAuth2(accountName)
|
||||
|
||||
override fun getBaseUrl(accountName: String): String = localAuthenticationDataSource.getBaseUrl(accountName)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.datasources
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
|
||||
interface LocalCapabilitiesDataSource {
|
||||
fun getCapabilitiesForAccountAsLiveData(
|
||||
accountName: String
|
||||
): LiveData<OCCapability?>
|
||||
|
||||
fun getCapabilitiesForAccount(
|
||||
accountName: String
|
||||
): OCCapability?
|
||||
|
||||
fun insertCapabilities(ocCapabilities: List<OCCapability>)
|
||||
|
||||
fun deleteCapabilitiesForAccount(accountName: String)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.datasources
|
||||
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
|
||||
interface RemoteCapabilitiesDataSource {
|
||||
fun getCapabilities(
|
||||
accountName: String
|
||||
): OCCapability
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.map
|
||||
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
|
||||
import eu.qsfera.android.data.capabilities.db.OCCapabilityDao
|
||||
import eu.qsfera.android.data.capabilities.db.OCCapabilityEntity
|
||||
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
|
||||
class OCLocalCapabilitiesDataSource(
|
||||
private val ocCapabilityDao: OCCapabilityDao,
|
||||
) : LocalCapabilitiesDataSource {
|
||||
|
||||
override fun getCapabilitiesForAccountAsLiveData(accountName: String): LiveData<OCCapability?> =
|
||||
ocCapabilityDao.getCapabilitiesForAccountAsLiveData(accountName).map { ocCapabilityEntity ->
|
||||
ocCapabilityEntity?.toModel()
|
||||
}
|
||||
|
||||
override fun getCapabilitiesForAccount(accountName: String): OCCapability? =
|
||||
ocCapabilityDao.getCapabilitiesForAccount(accountName)?.toModel()
|
||||
|
||||
override fun insertCapabilities(ocCapabilities: List<OCCapability>) {
|
||||
ocCapabilityDao.replace(
|
||||
ocCapabilities.map { ocCapability -> ocCapability.toEntity() }
|
||||
)
|
||||
}
|
||||
|
||||
override fun deleteCapabilitiesForAccount(accountName: String) {
|
||||
ocCapabilityDao.deleteByAccountName(accountName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun OCCapabilityEntity.toModel(): OCCapability =
|
||||
OCCapability(
|
||||
id = id,
|
||||
accountName = accountName,
|
||||
versionMajor = versionMajor,
|
||||
versionMinor = versionMinor,
|
||||
versionMicro = versionMicro,
|
||||
versionString = versionString,
|
||||
versionEdition = versionEdition,
|
||||
corePollInterval = corePollInterval,
|
||||
davChunkingVersion = davChunkingVersion,
|
||||
filesSharingApiEnabled = CapabilityBooleanType.fromValue(filesSharingApiEnabled),
|
||||
filesSharingPublicEnabled = CapabilityBooleanType.fromValue(filesSharingPublicEnabled),
|
||||
filesSharingPublicPasswordEnforced = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforced),
|
||||
filesSharingPublicPasswordEnforcedReadOnly = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforcedReadOnly),
|
||||
filesSharingPublicPasswordEnforcedReadWrite = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforcedReadWrite),
|
||||
filesSharingPublicPasswordEnforcedUploadOnly = CapabilityBooleanType.fromValue(filesSharingPublicPasswordEnforcedUploadOnly),
|
||||
filesSharingPublicExpireDateEnabled = CapabilityBooleanType.fromValue(filesSharingPublicExpireDateEnabled),
|
||||
filesSharingPublicExpireDateDays = filesSharingPublicExpireDateDays,
|
||||
filesSharingPublicExpireDateEnforced = CapabilityBooleanType.fromValue(filesSharingPublicExpireDateEnforced),
|
||||
filesSharingPublicUpload = CapabilityBooleanType.fromValue(filesSharingPublicUpload),
|
||||
filesSharingPublicMultiple = CapabilityBooleanType.fromValue(filesSharingPublicMultiple),
|
||||
filesSharingPublicSupportsUploadOnly = CapabilityBooleanType.fromValue(filesSharingPublicSupportsUploadOnly),
|
||||
filesSharingResharing = CapabilityBooleanType.fromValue(filesSharingResharing),
|
||||
filesSharingFederationOutgoing = CapabilityBooleanType.fromValue(filesSharingFederationOutgoing),
|
||||
filesSharingFederationIncoming = CapabilityBooleanType.fromValue(filesSharingFederationIncoming),
|
||||
filesSharingUserProfilePicture = CapabilityBooleanType.fromValue(filesSharingUserProfilePicture),
|
||||
filesBigFileChunking = CapabilityBooleanType.fromValue(filesBigFileChunking),
|
||||
filesUndelete = CapabilityBooleanType.fromValue(filesUndelete),
|
||||
filesVersioning = CapabilityBooleanType.fromValue(filesVersioning),
|
||||
filesPrivateLinks = CapabilityBooleanType.fromValue(filesPrivateLinks),
|
||||
filesAppProviders = appProviders,
|
||||
filesTusSupport = tusSupport,
|
||||
spaces = spaces,
|
||||
passwordPolicy = passwordPolicy,
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun OCCapability.toEntity(): OCCapabilityEntity =
|
||||
OCCapabilityEntity(
|
||||
accountName = accountName,
|
||||
versionMajor = versionMajor,
|
||||
versionMinor = versionMinor,
|
||||
versionMicro = versionMicro,
|
||||
versionString = versionString,
|
||||
versionEdition = versionEdition,
|
||||
corePollInterval = corePollInterval,
|
||||
davChunkingVersion = davChunkingVersion,
|
||||
filesSharingApiEnabled = filesSharingApiEnabled.value,
|
||||
filesSharingPublicEnabled = filesSharingPublicEnabled.value,
|
||||
filesSharingPublicPasswordEnforced = filesSharingPublicPasswordEnforced.value,
|
||||
filesSharingPublicPasswordEnforcedReadOnly = filesSharingPublicPasswordEnforcedReadOnly.value,
|
||||
filesSharingPublicPasswordEnforcedReadWrite = filesSharingPublicPasswordEnforcedReadWrite.value,
|
||||
filesSharingPublicPasswordEnforcedUploadOnly = filesSharingPublicPasswordEnforcedUploadOnly.value,
|
||||
filesSharingPublicExpireDateEnabled = filesSharingPublicExpireDateEnabled.value,
|
||||
filesSharingPublicExpireDateDays = filesSharingPublicExpireDateDays,
|
||||
filesSharingPublicExpireDateEnforced = filesSharingPublicExpireDateEnforced.value,
|
||||
filesSharingPublicUpload = filesSharingPublicUpload.value,
|
||||
filesSharingPublicMultiple = filesSharingPublicMultiple.value,
|
||||
filesSharingPublicSupportsUploadOnly = filesSharingPublicSupportsUploadOnly.value,
|
||||
filesSharingResharing = filesSharingResharing.value,
|
||||
filesSharingFederationOutgoing = filesSharingFederationOutgoing.value,
|
||||
filesSharingFederationIncoming = filesSharingFederationIncoming.value,
|
||||
filesSharingUserProfilePicture = filesSharingUserProfilePicture.value,
|
||||
filesBigFileChunking = filesBigFileChunking.value,
|
||||
filesUndelete = filesUndelete.value,
|
||||
filesVersioning = filesVersioning.value,
|
||||
filesPrivateLinks = filesPrivateLinks.value,
|
||||
appProviders = filesAppProviders,
|
||||
tusSupport = filesTusSupport,
|
||||
spaces = spaces,
|
||||
passwordPolicy = passwordPolicy,
|
||||
)
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.capabilities.datasources.RemoteCapabilitiesDataSource
|
||||
import eu.qsfera.android.data.capabilities.datasources.mapper.RemoteCapabilityMapper
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
|
||||
class OCRemoteCapabilitiesDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
private val remoteCapabilityMapper: RemoteCapabilityMapper
|
||||
) : RemoteCapabilitiesDataSource {
|
||||
|
||||
override fun getCapabilities(
|
||||
accountName: String
|
||||
): OCCapability {
|
||||
executeRemoteOperation {
|
||||
clientManager.getCapabilityService(accountName).getCapabilities()
|
||||
}.let { remoteCapability ->
|
||||
return remoteCapabilityMapper.toModel(remoteCapability)!!.apply {
|
||||
this.accountName = accountName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.datasources.mapper
|
||||
|
||||
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
import eu.qsfera.android.domain.mappers.RemoteMapper
|
||||
import eu.qsfera.android.lib.resources.status.RemoteCapability
|
||||
import eu.qsfera.android.lib.resources.status.RemoteCapability.CapabilityBooleanType as RemoteCapabilityBooleanType
|
||||
|
||||
class RemoteCapabilityMapper : RemoteMapper<OCCapability, RemoteCapability> {
|
||||
override fun toModel(remote: RemoteCapability?): OCCapability? =
|
||||
remote?.let {
|
||||
OCCapability(
|
||||
accountName = remote.accountName,
|
||||
versionMajor = remote.versionMajor,
|
||||
versionMinor = remote.versionMinor,
|
||||
versionMicro = remote.versionMicro,
|
||||
versionString = remote.versionString,
|
||||
versionEdition = remote.versionEdition,
|
||||
corePollInterval = remote.corePollinterval,
|
||||
davChunkingVersion = remote.chunkingVersion,
|
||||
filesSharingApiEnabled = CapabilityBooleanType.fromValue(remote.filesSharingApiEnabled.value),
|
||||
filesSharingPublicEnabled = CapabilityBooleanType.fromValue(remote.filesSharingPublicEnabled.value),
|
||||
filesSharingPublicPasswordEnforced =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforced.value),
|
||||
filesSharingPublicPasswordEnforcedReadOnly =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforcedReadOnly.value),
|
||||
filesSharingPublicPasswordEnforcedReadWrite =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforcedReadWrite.value),
|
||||
filesSharingPublicPasswordEnforcedUploadOnly =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicPasswordEnforcedUploadOnly.value),
|
||||
filesSharingPublicExpireDateEnabled =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicExpireDateEnabled.value),
|
||||
filesSharingPublicExpireDateDays = remote.filesSharingPublicExpireDateDays,
|
||||
filesSharingPublicExpireDateEnforced =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicExpireDateEnforced.value),
|
||||
filesSharingPublicUpload = CapabilityBooleanType.fromValue(remote.filesSharingPublicUpload.value),
|
||||
filesSharingPublicMultiple = CapabilityBooleanType.fromValue(remote.filesSharingPublicMultiple.value),
|
||||
filesSharingPublicSupportsUploadOnly =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingPublicSupportsUploadOnly.value),
|
||||
filesSharingResharing = CapabilityBooleanType.fromValue(remote.filesSharingResharing.value),
|
||||
filesSharingFederationOutgoing =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingFederationOutgoing.value),
|
||||
filesSharingFederationIncoming =
|
||||
CapabilityBooleanType.fromValue(remote.filesSharingFederationIncoming.value),
|
||||
filesSharingUserProfilePicture = CapabilityBooleanType.fromValue(remote.filesSharingUserProfilePicture.value),
|
||||
filesBigFileChunking = CapabilityBooleanType.fromValue(remote.filesBigFileChunking.value),
|
||||
filesUndelete = CapabilityBooleanType.fromValue(remote.filesUndelete.value),
|
||||
filesVersioning = CapabilityBooleanType.fromValue(remote.filesVersioning.value),
|
||||
filesPrivateLinks = CapabilityBooleanType.fromValue(remote.filesPrivateLinks.value),
|
||||
filesAppProviders = remote.filesAppProviders?.firstOrNull()?.toAppProviders(),
|
||||
filesTusSupport = remote.filesTusSupport?.toTusSupport(),
|
||||
spaces = remote.spaces?.toSpaces(),
|
||||
passwordPolicy = remote.passwordPolicy?.toPasswordPolicy()
|
||||
)
|
||||
}
|
||||
|
||||
override fun toRemote(model: OCCapability?): RemoteCapability? =
|
||||
model?.let {
|
||||
RemoteCapability(
|
||||
accountName = model.accountName!!,
|
||||
versionMajor = model.versionMajor,
|
||||
versionMinor = model.versionMinor,
|
||||
versionMicro = model.versionMicro,
|
||||
versionString = model.versionString!!,
|
||||
versionEdition = model.versionEdition!!,
|
||||
chunkingVersion = model.davChunkingVersion,
|
||||
corePollinterval = model.corePollInterval,
|
||||
filesSharingApiEnabled = RemoteCapabilityBooleanType.fromValue(model.filesSharingApiEnabled.value)!!,
|
||||
filesSharingPublicEnabled = RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicEnabled.value)!!,
|
||||
filesSharingPublicPasswordEnforced =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforced.value)!!,
|
||||
filesSharingPublicPasswordEnforcedReadOnly =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforcedReadOnly.value)!!,
|
||||
filesSharingPublicPasswordEnforcedReadWrite =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforcedReadWrite.value)!!,
|
||||
filesSharingPublicPasswordEnforcedUploadOnly =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicPasswordEnforcedUploadOnly.value)!!,
|
||||
filesSharingPublicExpireDateEnabled =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicExpireDateEnabled.value)!!,
|
||||
filesSharingPublicExpireDateDays = model.filesSharingPublicExpireDateDays,
|
||||
filesSharingPublicExpireDateEnforced =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicExpireDateEnforced.value)!!,
|
||||
filesSharingPublicUpload = RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicUpload.value)!!,
|
||||
filesSharingPublicMultiple = RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicMultiple.value)!!,
|
||||
filesSharingPublicSupportsUploadOnly =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingPublicSupportsUploadOnly.value)!!,
|
||||
filesSharingResharing = RemoteCapabilityBooleanType.fromValue(model.filesSharingResharing.value)!!,
|
||||
filesSharingFederationOutgoing =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingFederationOutgoing.value)!!,
|
||||
filesSharingFederationIncoming =
|
||||
RemoteCapabilityBooleanType.fromValue(model.filesSharingFederationIncoming.value)!!,
|
||||
filesSharingUserProfilePicture = RemoteCapabilityBooleanType.fromValue(model.filesSharingUserProfilePicture.value)!!,
|
||||
filesBigFileChunking = RemoteCapabilityBooleanType.fromValue(model.filesBigFileChunking.value)!!,
|
||||
filesUndelete = RemoteCapabilityBooleanType.fromValue(model.filesUndelete.value)!!,
|
||||
filesVersioning = RemoteCapabilityBooleanType.fromValue(model.filesVersioning.value)!!,
|
||||
filesPrivateLinks = RemoteCapabilityBooleanType.fromValue(model.filesPrivateLinks.value)!!,
|
||||
filesAppProviders = null,
|
||||
filesTusSupport = model.filesTusSupport?.toRemoteTusSupport(),
|
||||
spaces = null,
|
||||
passwordPolicy = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun RemoteCapability.RemoteAppProviders.toAppProviders() =
|
||||
OCCapability.AppProviders(enabled, version, appsUrl, openUrl, openWebUrl, newUrl)
|
||||
|
||||
private fun RemoteCapability.TusSupport.toTusSupport() =
|
||||
OCCapability.TusSupport(version, resumable, extension, maxChunkSize, httpMethodOverride)
|
||||
|
||||
private fun OCCapability.TusSupport.toRemoteTusSupport() =
|
||||
RemoteCapability.TusSupport(version, resumable, extension, maxChunkSize, httpMethodOverride)
|
||||
|
||||
private fun RemoteCapability.RemoteSpaces.toSpaces() =
|
||||
OCCapability.Spaces(enabled, projects, shareJail, hasMultiplePersonalSpaces)
|
||||
|
||||
private fun RemoteCapability.RemotePasswordPolicy.toPasswordPolicy() =
|
||||
OCCapability.PasswordPolicy(maxCharacters, minCharacters, minDigits, minLowercaseCharacters, minSpecialCharacters, minUppercaseCharacters)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Dao
|
||||
interface OCCapabilityDao {
|
||||
@Query(SELECT)
|
||||
fun getCapabilitiesForAccountAsLiveData(
|
||||
accountName: String
|
||||
): LiveData<OCCapabilityEntity?>
|
||||
|
||||
@Query(SELECT)
|
||||
fun getCapabilitiesForAccount(
|
||||
accountName: String
|
||||
): OCCapabilityEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(ocCapability: OCCapabilityEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(ocCapabilities: List<OCCapabilityEntity>): List<Long>
|
||||
|
||||
@Query(DELETE_CAPABILITIES_BY_ACCOUNTNAME)
|
||||
fun deleteByAccountName(accountName: String)
|
||||
|
||||
@Transaction
|
||||
fun replace(ocCapabilities: List<OCCapabilityEntity>) {
|
||||
ocCapabilities.forEach { ocCapability ->
|
||||
ocCapability.accountName?.run {
|
||||
deleteByAccountName(this)
|
||||
}
|
||||
}
|
||||
insertOrReplace(ocCapabilities)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SELECT = """
|
||||
SELECT *
|
||||
FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME} = :accountName
|
||||
"""
|
||||
private const val DELETE_CAPABILITIES_BY_ACCOUNTNAME = """
|
||||
DELETE
|
||||
FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME} = :accountName
|
||||
"""
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.db
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_APP_PROVIDERS_PREFIX
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_CORE_POLLINTERVAL
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_DAV_CHUNKING_VERSION
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_BIGFILECHUNKING
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_PRIVATE_LINKS
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_UNDELETE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_FILES_VERSIONING
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_PREFIX
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_PASSWORD_POLICY_PREFIX
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_API_ENABLED
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_INCOMING
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_FEDERATION_OUTGOING
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_ENABLED
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_MULTIPLE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_PUBLIC_UPLOAD
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_RESHARING
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SHARING_USER_PROFILE_PICTURE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_SPACES_PREFIX
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_EDITION
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MAJOR
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MICRO
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MINOR
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_STRING
|
||||
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType
|
||||
import eu.qsfera.android.domain.capabilities.model.CapabilityBooleanType.Companion.capabilityBooleanTypeUnknownString
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
|
||||
/**
|
||||
* Represents one record of the Capabilities table.
|
||||
*/
|
||||
@Entity(tableName = CAPABILITIES_TABLE_NAME)
|
||||
data class OCCapabilityEntity(
|
||||
@ColumnInfo(name = CAPABILITIES_ACCOUNT_NAME)
|
||||
val accountName: String?,
|
||||
@ColumnInfo(name = CAPABILITIES_VERSION_MAJOR)
|
||||
val versionMajor: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_VERSION_MINOR)
|
||||
val versionMinor: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_VERSION_MICRO)
|
||||
val versionMicro: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_VERSION_STRING)
|
||||
val versionString: String?,
|
||||
@ColumnInfo(name = CAPABILITIES_VERSION_EDITION)
|
||||
val versionEdition: String?,
|
||||
@ColumnInfo(name = CAPABILITIES_CORE_POLLINTERVAL)
|
||||
val corePollInterval: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_DAV_CHUNKING_VERSION)
|
||||
val davChunkingVersion: String,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_API_ENABLED, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingApiEnabled: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_ENABLED, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicEnabled: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicPasswordEnforced: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicPasswordEnforcedReadOnly: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicPasswordEnforcedReadWrite: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicPasswordEnforcedUploadOnly: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicExpireDateEnabled: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS)
|
||||
val filesSharingPublicExpireDateDays: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicExpireDateEnforced: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_UPLOAD, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicUpload: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_MULTIPLE, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicMultiple: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingPublicSupportsUploadOnly: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_RESHARING, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingResharing: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_FEDERATION_OUTGOING, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingFederationOutgoing: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_FEDERATION_INCOMING, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingFederationIncoming: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_SHARING_USER_PROFILE_PICTURE, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesSharingUserProfilePicture: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_FILES_BIGFILECHUNKING, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesBigFileChunking: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_FILES_UNDELETE, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesUndelete: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_FILES_VERSIONING, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesVersioning: Int,
|
||||
@ColumnInfo(name = CAPABILITIES_FILES_PRIVATE_LINKS, defaultValue = capabilityBooleanTypeUnknownString)
|
||||
val filesPrivateLinks: Int,
|
||||
@Embedded(prefix = CAPABILITIES_APP_PROVIDERS_PREFIX)
|
||||
val appProviders: OCCapability.AppProviders?,
|
||||
@Embedded(prefix = CAPABILITIES_TUS_SUPPORT_PREFIX)
|
||||
val tusSupport: OCCapability.TusSupport?,
|
||||
@Embedded(prefix = CAPABILITIES_SPACES_PREFIX)
|
||||
val spaces: OCCapability.Spaces?,
|
||||
@Embedded(prefix = CAPABILITIES_PASSWORD_POLICY_PREFIX)
|
||||
val passwordPolicy: OCCapability.PasswordPolicy?,
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
|
||||
companion object {
|
||||
fun fromCursor(cursor: Cursor): OCCapabilityEntity = cursor.use {
|
||||
OCCapabilityEntity(
|
||||
it.getString(it.getColumnIndexOrThrow(CAPABILITIES_ACCOUNT_NAME)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_MAJOR)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_MINOR)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_MICRO)),
|
||||
it.getString(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_STRING)),
|
||||
it.getString(it.getColumnIndexOrThrow(CAPABILITIES_VERSION_EDITION)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_CORE_POLLINTERVAL)),
|
||||
it.getColumnIndex(CAPABILITIES_DAV_CHUNKING_VERSION).takeUnless { it < 0 }?.let { index -> it.getString(index) }.orEmpty(),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_API_ENABLED)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_ENABLED)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_ONLY)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_READ_WRITE)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_PASSWORD_ENFORCED_UPLOAD_ONLY)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENABLED)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_DAYS)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_EXPIRE_DATE_ENFORCED)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_UPLOAD)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_MULTIPLE)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_PUBLIC_SUPPORTS_UPLOAD_ONLY)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_RESHARING)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_FEDERATION_OUTGOING)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_FEDERATION_INCOMING)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_SHARING_USER_PROFILE_PICTURE)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_FILES_BIGFILECHUNKING)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_FILES_UNDELETE)),
|
||||
it.getInt(it.getColumnIndexOrThrow(CAPABILITIES_FILES_VERSIONING)),
|
||||
it.getColumnIndex(CAPABILITIES_FILES_PRIVATE_LINKS).takeUnless { it < 0 }?.let { index -> it.getInt(index) }
|
||||
?: CapabilityBooleanType.UNKNOWN.value,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.capabilities.repository
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
|
||||
import eu.qsfera.android.data.capabilities.datasources.RemoteCapabilitiesDataSource
|
||||
import eu.qsfera.android.domain.appregistry.AppRegistryRepository
|
||||
import eu.qsfera.android.domain.capabilities.CapabilityRepository
|
||||
import eu.qsfera.android.domain.capabilities.model.OCCapability
|
||||
|
||||
class OCCapabilityRepository(
|
||||
private val localCapabilitiesDataSource: LocalCapabilitiesDataSource,
|
||||
private val remoteCapabilitiesDataSource: RemoteCapabilitiesDataSource,
|
||||
private val appRegistryRepository: AppRegistryRepository,
|
||||
) : CapabilityRepository {
|
||||
|
||||
override fun getCapabilitiesAsLiveData(accountName: String): LiveData<OCCapability?> =
|
||||
localCapabilitiesDataSource.getCapabilitiesForAccountAsLiveData(accountName)
|
||||
|
||||
override fun getStoredCapabilities(
|
||||
accountName: String
|
||||
): OCCapability? = localCapabilitiesDataSource.getCapabilitiesForAccount(accountName)
|
||||
|
||||
override fun refreshCapabilitiesForAccount(
|
||||
accountName: String
|
||||
) {
|
||||
val capabilitiesFromNetwork = remoteCapabilitiesDataSource.getCapabilities(accountName)
|
||||
localCapabilitiesDataSource.insertCapabilities(listOf(capabilitiesFromNetwork))
|
||||
|
||||
if (capabilitiesFromNetwork.filesAppProviders != null) {
|
||||
appRegistryRepository.refreshAppRegistryForAccount(accountName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Fernando Sanz Velasco
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.extensions
|
||||
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* It's basically a copy of copyRecursively() but deleting the source file after copying it
|
||||
* to the target location
|
||||
*/
|
||||
fun File.moveRecursively(
|
||||
target: File,
|
||||
overwrite: Boolean = false,
|
||||
onError: (File, IOException) -> OnErrorAction = { _, exception -> throw exception }
|
||||
): Boolean {
|
||||
if (!exists()) {
|
||||
return onError(this, NoSuchFileException(file = this, reason = "The source file doesn't exist.")) !=
|
||||
OnErrorAction.TERMINATE
|
||||
}
|
||||
try {
|
||||
// We cannot break for loop from inside a lambda, so we have to use an exception here
|
||||
for (src in walkTopDown().onFail { f, e -> if (onError(f, e) == OnErrorAction.TERMINATE) throw TerminateException(f) }) {
|
||||
if (!src.exists()) {
|
||||
if (onError(src, NoSuchFileException(file = src, reason = "The source file doesn't exist.")) ==
|
||||
OnErrorAction.TERMINATE
|
||||
)
|
||||
return false
|
||||
} else {
|
||||
val relPath = src.toRelativeString(this)
|
||||
val dstFile = File(target, relPath)
|
||||
if (dstFile.exists() && !(src.isDirectory && dstFile.isDirectory)) {
|
||||
val stillExists = if (!overwrite) {
|
||||
true
|
||||
} else {
|
||||
if (dstFile.isDirectory)
|
||||
!dstFile.deleteRecursively()
|
||||
else
|
||||
!dstFile.delete()
|
||||
}
|
||||
|
||||
if (stillExists) {
|
||||
if (onError(
|
||||
dstFile, FileAlreadyExistsException(
|
||||
file = src,
|
||||
other = dstFile,
|
||||
reason = "The destination file already exists."
|
||||
)
|
||||
) == OnErrorAction.TERMINATE
|
||||
)
|
||||
return false
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (src.isDirectory) {
|
||||
dstFile.mkdirs()
|
||||
} else {
|
||||
try {
|
||||
if (src.copyTo(dstFile, overwrite).length() != src.length()) {
|
||||
if (onError(
|
||||
src,
|
||||
IOException("Source file wasn't copied completely, length of destination file differs.")
|
||||
) == OnErrorAction.TERMINATE
|
||||
)
|
||||
return false
|
||||
} else {
|
||||
src.delete()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "An error occurred while trying to move the file")
|
||||
src.delete()
|
||||
dstFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
} catch (e: TerminateException) {
|
||||
Timber.e(e, "The process terminated unexpectedly")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//Private exception class, used to terminate recursive copying.
|
||||
private class TerminateException(file: File) : FileSystemException(file)
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Christian Schabesberger
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.datasources
|
||||
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.util.UUID
|
||||
|
||||
interface LocalFileDataSource {
|
||||
fun getFileById(fileId: Long): OCFile?
|
||||
fun getFileByIdAsFlow(fileId: Long): Flow<OCFile?>
|
||||
fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile?
|
||||
fun getFileByRemoteId(remoteId: String): OCFile?
|
||||
fun getFolderContent(folderId: Long): List<OCFile>
|
||||
fun getSearchFolderContent(folderId: Long, search: String): List<OCFile>
|
||||
fun getSearchAvailableOfflineFolderContent(folderId: Long, search: String): List<OCFile>
|
||||
fun getSearchSharedByLinkFolderContent(folderId: Long, search: String): List<OCFile>
|
||||
fun getFolderContentWithSyncInfoAsFlow(folderId: Long): Flow<List<OCFileWithSyncInfo>>
|
||||
fun getFolderImages(folderId: Long): List<OCFile>
|
||||
fun getSharedByLinkWithSyncInfoForAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>>
|
||||
fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>>
|
||||
fun getFilesAvailableOfflineFromAccount(owner: String): List<OCFile>
|
||||
fun getFilesAvailableOfflineFromEveryAccount(): List<OCFile>
|
||||
fun getDownloadedFilesForAccount(owner: String): List<OCFile>
|
||||
fun getFileWithSyncInfoByIdAsFlow(id: Long): Flow<OCFileWithSyncInfo?>
|
||||
fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFile>
|
||||
fun moveFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, finalStoragePath: String)
|
||||
fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String, replace: Boolean?)
|
||||
fun saveFilesInFolderAndReturnTheFilesThatChanged(listOfFiles: List<OCFile>, folder: OCFile): List<OCFile>
|
||||
fun saveFile(file: OCFile)
|
||||
fun saveConflict(fileId: Long, eTagInConflict: String)
|
||||
fun cleanConflict(fileId: Long)
|
||||
fun deleteFile(fileId: Long)
|
||||
fun deleteFilesForAccount(accountName: String)
|
||||
fun renameFile(fileToRename: OCFile, finalRemotePath: String, finalStoragePath: String)
|
||||
|
||||
fun disableThumbnailsForFile(fileId: Long)
|
||||
fun updateAvailableOfflineStatusForFile(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus)
|
||||
fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String)
|
||||
fun saveUploadWorkerUuid(fileId: Long, workerUuid: UUID)
|
||||
fun saveDownloadWorkerUuid(fileId: Long, workerUuid: UUID)
|
||||
fun cleanWorkersUuid(fileId: Long)
|
||||
fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?)
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.datasources
|
||||
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.files.model.OCMetaFile
|
||||
|
||||
interface RemoteFileDataSource {
|
||||
fun checkPathExistence(
|
||||
path: String,
|
||||
isUserLogged: Boolean,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
): Boolean
|
||||
|
||||
fun copyFile(
|
||||
sourceRemotePath: String,
|
||||
targetRemotePath: String,
|
||||
accountName: String,
|
||||
sourceSpaceWebDavUrl: String?,
|
||||
targetSpaceWebDavUrl: String?,
|
||||
replace: Boolean,
|
||||
): String?
|
||||
|
||||
fun createFolder(
|
||||
remotePath: String,
|
||||
createFullPath: Boolean,
|
||||
isChunksFolder: Boolean,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
)
|
||||
|
||||
fun getAvailableRemotePath(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
isUserLogged: Boolean,
|
||||
): String
|
||||
|
||||
fun moveFile(
|
||||
sourceRemotePath: String,
|
||||
targetRemotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
replace: Boolean,
|
||||
)
|
||||
|
||||
fun readFile(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String? = null,
|
||||
): OCFile
|
||||
|
||||
fun refreshFolder(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String? = null,
|
||||
): List<OCFile>
|
||||
|
||||
fun deleteFile(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String? = null,
|
||||
)
|
||||
|
||||
fun renameFile(
|
||||
oldName: String,
|
||||
oldRemotePath: String,
|
||||
newName: String,
|
||||
isFolder: Boolean,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String? = null,
|
||||
)
|
||||
|
||||
fun getMetaFile(
|
||||
fileId: String,
|
||||
accountName: String,
|
||||
): OCMetaFile
|
||||
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.files.datasources.LocalFileDataSource
|
||||
import eu.qsfera.android.data.files.db.FileDao
|
||||
import eu.qsfera.android.data.files.db.OCFileAndFileSync
|
||||
import eu.qsfera.android.data.files.db.OCFileEntity
|
||||
import eu.qsfera.android.data.spaces.datasources.implementation.OCLocalSpacesDataSource.Companion.toModel
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus
|
||||
import eu.qsfera.android.domain.files.model.MIME_DIR
|
||||
import eu.qsfera.android.domain.files.model.MIME_PREFIX_IMAGE
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID
|
||||
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PATH
|
||||
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.util.UUID
|
||||
|
||||
class OCLocalFileDataSource(
|
||||
private val fileDao: FileDao,
|
||||
) : LocalFileDataSource {
|
||||
override fun getFileById(fileId: Long): OCFile? =
|
||||
fileDao.getFileById(fileId)?.toModel()
|
||||
|
||||
override fun getFileByIdAsFlow(fileId: Long): Flow<OCFile?> =
|
||||
fileDao.getFileByIdAsFlow(fileId).map { it?.toModel() }
|
||||
|
||||
override fun getFileWithSyncInfoByIdAsFlow(id: Long): Flow<OCFileWithSyncInfo?> =
|
||||
fileDao.getFileWithSyncInfoByIdAsFlow(id).map { it?.toModel() }
|
||||
|
||||
override fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? {
|
||||
fileDao.getFileByOwnerAndRemotePath(owner, remotePath, spaceId)?.let { return it.toModel() }
|
||||
|
||||
// If root folder do not exists, create and return it.
|
||||
return if (remotePath == ROOT_PATH) {
|
||||
val rootFolder = OCFile(
|
||||
parentId = ROOT_PARENT_ID,
|
||||
owner = owner,
|
||||
remotePath = ROOT_PATH,
|
||||
length = 0,
|
||||
mimeType = MIME_DIR,
|
||||
modificationTimestamp = 0,
|
||||
spaceId = spaceId,
|
||||
permissions = "CK",
|
||||
)
|
||||
val idFile = fileDao.mergeRemoteAndLocalFile(rootFolder.toEntity())
|
||||
getFileById(idFile)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFileByRemoteId(remoteId: String): OCFile? =
|
||||
fileDao.getFileByRemoteId(remoteId)?.toModel()
|
||||
|
||||
override fun getFolderContent(folderId: Long): List<OCFile> =
|
||||
fileDao.getFolderContent(folderId = folderId).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getSearchFolderContent(folderId: Long, search: String): List<OCFile> =
|
||||
fileDao.getSearchFolderContent(folderId = folderId, search = search).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getSearchAvailableOfflineFolderContent(folderId: Long, search: String): List<OCFile> =
|
||||
fileDao.getSearchAvailableOfflineFolderContent(folderId = folderId, search = search).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getSearchSharedByLinkFolderContent(folderId: Long, search: String): List<OCFile> =
|
||||
fileDao.getSearchSharedByLinkFolderContent(folderId = folderId, search = search).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getFolderContentWithSyncInfoAsFlow(folderId: Long): Flow<List<OCFileWithSyncInfo>> =
|
||||
fileDao.getFolderContentWithSyncInfoAsFlow(folderId = folderId).map { folderContent ->
|
||||
folderContent.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun getFolderImages(folderId: Long): List<OCFile> =
|
||||
fileDao.getFolderByMimeType(folderId = folderId, mimeType = MIME_PREFIX_IMAGE).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getSharedByLinkWithSyncInfoForAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
|
||||
fileDao.getFilesWithSyncInfoSharedByLinkAsFlow(accountOwner = owner).map { fileList ->
|
||||
fileList.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
|
||||
fileDao.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner).map { fileList ->
|
||||
fileList.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun getFilesAvailableOfflineFromAccount(owner: String): List<OCFile> =
|
||||
fileDao.getFilesAvailableOfflineFromAccount(accountOwner = owner).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getFilesAvailableOfflineFromEveryAccount(): List<OCFile> =
|
||||
fileDao.getFilesAvailableOfflineFromEveryAccount().map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getDownloadedFilesForAccount(owner: String): List<OCFile> =
|
||||
fileDao.getDownloadedFilesForAccount(accountOwner = owner).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFile> =
|
||||
fileDao.getFilesWithLastUsageOlderThanGivenTime(milliseconds).map {
|
||||
it.toModel()
|
||||
}
|
||||
|
||||
override fun moveFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, finalStoragePath: String) =
|
||||
fileDao.moveFile(
|
||||
sourceFile = sourceFile.toEntity(),
|
||||
targetFolder = targetFolder.toEntity(),
|
||||
finalRemotePath = finalRemotePath,
|
||||
finalStoragePath = finalStoragePath
|
||||
)
|
||||
|
||||
override fun copyFile(sourceFile: OCFile, targetFolder: OCFile, finalRemotePath: String, remoteId: String, replace: Boolean?) {
|
||||
fileDao.copy(
|
||||
sourceFile = sourceFile.toEntity(),
|
||||
targetFolder = targetFolder.toEntity(),
|
||||
finalRemotePath = finalRemotePath,
|
||||
remoteId = remoteId,
|
||||
replace = replace,
|
||||
)
|
||||
}
|
||||
|
||||
override fun saveFilesInFolderAndReturnTheFilesThatChanged(listOfFiles: List<OCFile>, folder: OCFile): List<OCFile> {
|
||||
// To do: If it is root, add 0 as parent Id
|
||||
val folderContent = fileDao.insertFilesInFolderAndReturnTheFilesThatChanged(
|
||||
folder = folder.toEntity(),
|
||||
folderContent = listOfFiles.map { it.toEntity() }
|
||||
)
|
||||
return folderContent.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun saveFile(file: OCFile) {
|
||||
fileDao.upsert(file.toEntity())
|
||||
}
|
||||
|
||||
override fun saveConflict(fileId: Long, eTagInConflict: String) {
|
||||
fileDao.updateConflictStatusForFile(fileId, eTagInConflict)
|
||||
}
|
||||
|
||||
override fun cleanConflict(fileId: Long) {
|
||||
fileDao.updateConflictStatusForFile(fileId, null)
|
||||
}
|
||||
|
||||
override fun deleteFile(fileId: Long) {
|
||||
fileDao.deleteFileById(fileId)
|
||||
}
|
||||
|
||||
override fun deleteFilesForAccount(accountName: String) {
|
||||
fileDao.deleteFilesForAccount(accountName)
|
||||
}
|
||||
|
||||
override fun renameFile(fileToRename: OCFile, finalRemotePath: String, finalStoragePath: String) {
|
||||
fileDao.moveFile(
|
||||
sourceFile = fileToRename.toEntity(),
|
||||
targetFolder = fileDao.getFileById(fileToRename.parentId!!)!!,
|
||||
finalRemotePath = finalRemotePath,
|
||||
finalStoragePath = finalStoragePath
|
||||
)
|
||||
}
|
||||
|
||||
override fun disableThumbnailsForFile(fileId: Long) {
|
||||
fileDao.disableThumbnailsForFile(fileId)
|
||||
}
|
||||
|
||||
override fun updateAvailableOfflineStatusForFile(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus) {
|
||||
fileDao.updateAvailableOfflineStatusForFile(ocFile, newAvailableOfflineStatus.ordinal)
|
||||
}
|
||||
|
||||
override fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) {
|
||||
fileDao.updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory, newDirectory)
|
||||
}
|
||||
|
||||
override fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?) {
|
||||
fileDao.updateFileWithLastUsage(fileId, lastUsage)
|
||||
}
|
||||
|
||||
override fun saveUploadWorkerUuid(fileId: Long, workerUuid: UUID) {
|
||||
// Not yet implemented
|
||||
}
|
||||
|
||||
override fun saveDownloadWorkerUuid(fileId: Long, workerUuid: UUID) {
|
||||
fileDao.updateSyncStatusForFile(fileId, workerUuid)
|
||||
}
|
||||
|
||||
override fun cleanWorkersUuid(fileId: Long) {
|
||||
fileDao.updateSyncStatusForFile(fileId, null)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun OCFileAndFileSync.toModel(): OCFileWithSyncInfo =
|
||||
OCFileWithSyncInfo(
|
||||
file = file.toModel(),
|
||||
uploadWorkerUuid = fileSync?.uploadWorkerUuid,
|
||||
downloadWorkerUuid = fileSync?.downloadWorkerUuid,
|
||||
isSynchronizing = fileSync?.isSynchronizing == true,
|
||||
space = space?.toModel(),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun OCFileEntity.toModel(): OCFile =
|
||||
OCFile(
|
||||
id = id,
|
||||
parentId = parentId,
|
||||
remotePath = remotePath,
|
||||
owner = owner,
|
||||
permissions = permissions,
|
||||
remoteId = remoteId,
|
||||
privateLink = privateLink,
|
||||
creationTimestamp = creationTimestamp,
|
||||
modificationTimestamp = modificationTimestamp,
|
||||
etag = etag,
|
||||
remoteEtag = remoteEtag,
|
||||
mimeType = mimeType,
|
||||
length = length,
|
||||
sharedByLink = sharedByLink,
|
||||
sharedWithSharee = sharedWithSharee,
|
||||
storagePath = storagePath,
|
||||
availableOfflineStatus = AvailableOfflineStatus.fromValue(availableOfflineStatus),
|
||||
needsToUpdateThumbnail = needsToUpdateThumbnail,
|
||||
fileIsDownloading = fileIsDownloading,
|
||||
lastSyncDateForData = lastSyncDateForData,
|
||||
lastUsage = lastUsage,
|
||||
modifiedAtLastSyncForData = modifiedAtLastSyncForData,
|
||||
etagInConflict = etagInConflict,
|
||||
treeEtag = treeEtag,
|
||||
spaceId = spaceId,
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun OCFile.toEntity(): OCFileEntity =
|
||||
OCFileEntity(
|
||||
parentId = parentId,
|
||||
remotePath = remotePath,
|
||||
owner = owner,
|
||||
permissions = permissions,
|
||||
remoteId = remoteId,
|
||||
privateLink = privateLink,
|
||||
creationTimestamp = creationTimestamp,
|
||||
modificationTimestamp = modificationTimestamp,
|
||||
etag = etag,
|
||||
remoteEtag = remoteEtag,
|
||||
mimeType = mimeType,
|
||||
length = length,
|
||||
sharedByLink = sharedByLink,
|
||||
sharedWithSharee = sharedWithSharee,
|
||||
storagePath = storagePath,
|
||||
availableOfflineStatus = availableOfflineStatus?.ordinal ?: AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE.ordinal,
|
||||
needsToUpdateThumbnail = needsToUpdateThumbnail,
|
||||
fileIsDownloading = fileIsDownloading,
|
||||
lastSyncDateForData = lastSyncDateForData,
|
||||
lastUsage = lastUsage,
|
||||
modifiedAtLastSyncForData = modifiedAtLastSyncForData,
|
||||
etagInConflict = etagInConflict,
|
||||
treeEtag = treeEtag,
|
||||
name = fileName,
|
||||
spaceId = spaceId,
|
||||
).apply { this@toEntity.id?.let { modelId -> this.id = modelId } }
|
||||
}
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Manuel Plazas Palacio
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.files.datasources.RemoteFileDataSource
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.files.model.OCMetaFile
|
||||
import eu.qsfera.android.lib.resources.files.RemoteFile
|
||||
import eu.qsfera.android.lib.resources.files.RemoteMetaFile
|
||||
|
||||
class OCRemoteFileDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
) : RemoteFileDataSource {
|
||||
override fun checkPathExistence(
|
||||
path: String,
|
||||
isUserLogged: Boolean,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
): Boolean = clientManager.getFileService(accountName).checkPathExistence(
|
||||
path = path,
|
||||
isUserLogged = isUserLogged,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
).data
|
||||
|
||||
override fun copyFile(
|
||||
sourceRemotePath: String,
|
||||
targetRemotePath: String,
|
||||
accountName: String,
|
||||
sourceSpaceWebDavUrl: String?,
|
||||
targetSpaceWebDavUrl: String?,
|
||||
replace: Boolean,
|
||||
): String? = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).copyFile(
|
||||
sourceRemotePath = sourceRemotePath,
|
||||
targetRemotePath = targetRemotePath,
|
||||
sourceSpaceWebDavUrl = sourceSpaceWebDavUrl,
|
||||
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
replace = replace,
|
||||
)
|
||||
}
|
||||
|
||||
override fun createFolder(
|
||||
remotePath: String,
|
||||
createFullPath: Boolean,
|
||||
isChunksFolder: Boolean,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
) = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).createFolder(
|
||||
remotePath = remotePath,
|
||||
createFullPath = createFullPath,
|
||||
isChunkFolder = isChunksFolder,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if remotePath does not exist in the server and returns it, or adds
|
||||
* a suffix to it in order to avoid the server file is overwritten.
|
||||
*
|
||||
* @param remotePath
|
||||
* @return
|
||||
*/
|
||||
override fun getAvailableRemotePath(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
isUserLogged: Boolean,
|
||||
): String {
|
||||
var checkExistsFile = checkPathExistence(
|
||||
path = remotePath,
|
||||
isUserLogged = isUserLogged,
|
||||
accountName = accountName,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
if (!checkExistsFile) {
|
||||
return remotePath
|
||||
}
|
||||
|
||||
val pos = remotePath.lastIndexOf(".")
|
||||
var suffix: String
|
||||
var extension = ""
|
||||
if (pos >= 0) {
|
||||
extension = remotePath.substring(pos + 1)
|
||||
remotePath.apply {
|
||||
substring(0, pos)
|
||||
}
|
||||
}
|
||||
var count = 1
|
||||
do {
|
||||
suffix = " ($count)"
|
||||
checkExistsFile = if (pos >= 0) {
|
||||
checkPathExistence(
|
||||
path = "${remotePath.substringBeforeLast('.', "")}$suffix.$extension",
|
||||
isUserLogged = isUserLogged,
|
||||
accountName = accountName,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
} else {
|
||||
checkPathExistence(
|
||||
path = "$remotePath$suffix",
|
||||
isUserLogged = isUserLogged,
|
||||
accountName = accountName,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
}
|
||||
count++
|
||||
} while (checkExistsFile)
|
||||
return if (pos >= 0) {
|
||||
"${remotePath.substringBeforeLast('.', "")}$suffix.$extension"
|
||||
} else {
|
||||
remotePath + suffix
|
||||
}
|
||||
}
|
||||
|
||||
override fun moveFile(
|
||||
sourceRemotePath: String,
|
||||
targetRemotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
replace: Boolean,
|
||||
) = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).moveFile(
|
||||
sourceRemotePath = sourceRemotePath,
|
||||
targetRemotePath = targetRemotePath,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
replace = replace,
|
||||
)
|
||||
}
|
||||
|
||||
override fun readFile(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
): OCFile = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).readFile(
|
||||
remotePath = remotePath,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
}.toModel()
|
||||
|
||||
override fun refreshFolder(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
): List<OCFile> =
|
||||
// Assert not null, service should return an empty list if no files there.
|
||||
executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).refreshFolder(
|
||||
remotePath = remotePath,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
}.let { listOfRemote ->
|
||||
listOfRemote.map { remoteFile -> remoteFile.toModel() }
|
||||
}
|
||||
|
||||
override fun deleteFile(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
) = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).removeFile(
|
||||
remotePath = remotePath,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
}
|
||||
|
||||
override fun renameFile(
|
||||
oldName: String,
|
||||
oldRemotePath: String,
|
||||
newName: String,
|
||||
isFolder: Boolean,
|
||||
accountName: String,
|
||||
spaceWebDavUrl: String?,
|
||||
) = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).renameFile(
|
||||
oldName = oldName,
|
||||
oldRemotePath = oldRemotePath,
|
||||
newName = newName,
|
||||
isFolder = isFolder,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getMetaFile(
|
||||
fileId: String,
|
||||
accountName: String,
|
||||
): OCMetaFile = executeRemoteOperation {
|
||||
clientManager.getFileService(accountName).getMetaFileInfo(fileId)
|
||||
}.toModel()
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun RemoteFile.toModel(): OCFile =
|
||||
OCFile(
|
||||
owner = owner,
|
||||
remoteId = remoteId,
|
||||
remotePath = if (isFolder && !remotePath.endsWith(OCFile.PATH_SEPARATOR)) {
|
||||
"$remotePath${OCFile.PATH_SEPARATOR}"
|
||||
} else {
|
||||
remotePath
|
||||
},
|
||||
length = if (isFolder) {
|
||||
size
|
||||
} else {
|
||||
length
|
||||
},
|
||||
creationTimestamp = creationTimestamp,
|
||||
modificationTimestamp = modifiedTimestamp,
|
||||
mimeType = mimeType,
|
||||
etag = etag,
|
||||
remoteEtag = etag,
|
||||
permissions = permissions,
|
||||
privateLink = privateLink,
|
||||
sharedWithSharee = sharedWithSharee,
|
||||
sharedByLink = sharedByLink,
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun RemoteMetaFile.toModel(): OCMetaFile =
|
||||
OCMetaFile(
|
||||
path = metaPathForUser,
|
||||
id = id,
|
||||
fileId = fileId,
|
||||
spaceId = spaceId,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import androidx.room.Upsert
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE
|
||||
import eu.qsfera.android.domain.extensions.isOneOf
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PARENT_ID
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import java.io.File.separatorChar
|
||||
import java.util.UUID
|
||||
|
||||
@Dao
|
||||
interface FileDao {
|
||||
@Query(SELECT_FILE_WITH_ID)
|
||||
fun getFileById(
|
||||
id: Long
|
||||
): OCFileEntity?
|
||||
|
||||
@Query(SELECT_FILE_WITH_ID)
|
||||
fun getFileByIdAsFlow(
|
||||
id: Long
|
||||
): Flow<OCFileEntity?>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_FILE_WITH_ID)
|
||||
fun getFileWithSyncInfoById(
|
||||
id: Long
|
||||
): OCFileAndFileSync?
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_FILE_WITH_ID)
|
||||
fun getFileWithSyncInfoByIdAsFlow(
|
||||
id: Long
|
||||
): Flow<OCFileAndFileSync?>
|
||||
|
||||
@Query(SELECT_FILE_FROM_OWNER_WITH_REMOTE_PATH)
|
||||
fun getFileByOwnerAndRemotePath(
|
||||
owner: String,
|
||||
remotePath: String,
|
||||
spaceId: String?,
|
||||
): OCFileEntity?
|
||||
|
||||
@Query(SELECT_FILE_WITH_REMOTE_ID)
|
||||
fun getFileByRemoteId(
|
||||
remoteId: String
|
||||
): OCFileEntity?
|
||||
|
||||
@Query(SELECT_FILTERED_FOLDER_CONTENT)
|
||||
fun getSearchFolderContent(
|
||||
folderId: Long,
|
||||
search: String
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Query(SELECT_FILTERED_AVAILABLE_OFFLINE_FOLDER_CONTENT)
|
||||
fun getSearchAvailableOfflineFolderContent(
|
||||
folderId: Long,
|
||||
search: String
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Query(SELECT_FILTERED_SHARED_BY_LINK_FOLDER_CONTENT)
|
||||
fun getSearchSharedByLinkFolderContent(
|
||||
folderId: Long,
|
||||
search: String
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Query(SELECT_FOLDER_CONTENT)
|
||||
fun getFolderContent(
|
||||
folderId: Long
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_FOLDER_CONTENT)
|
||||
fun getFolderContentWithSyncInfo(
|
||||
folderId: Long
|
||||
): List<OCFileAndFileSync>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_FOLDER_CONTENT)
|
||||
fun getFolderContentWithSyncInfoAsFlow(
|
||||
folderId: Long
|
||||
): Flow<List<OCFileAndFileSync>>
|
||||
|
||||
@Query(SELECT_FOLDER_BY_MIMETYPE)
|
||||
fun getFolderByMimeType(
|
||||
folderId: Long,
|
||||
mimeType: String
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_FILES_SHARED_BY_LINK)
|
||||
fun getFilesWithSyncInfoSharedByLinkAsFlow(
|
||||
accountOwner: String
|
||||
): Flow<List<OCFileAndFileSync>>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_FILES_AVAILABLE_OFFLINE_FROM_ACCOUNT)
|
||||
fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(
|
||||
accountOwner: String
|
||||
): Flow<List<OCFileAndFileSync>>
|
||||
|
||||
@Query(SELECT_FILES_AVAILABLE_OFFLINE_FROM_ACCOUNT)
|
||||
fun getFilesAvailableOfflineFromAccount(
|
||||
accountOwner: String
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Query(SELECT_FILES_AVAILABLE_OFFLINE_FROM_EVERY_ACCOUNT)
|
||||
fun getFilesAvailableOfflineFromEveryAccount(): List<OCFileEntity>
|
||||
|
||||
@Query(SELECT_DOWNLOADED_FILES_FOR_ACCOUNT)
|
||||
fun getDownloadedFilesForAccount(
|
||||
accountOwner: String
|
||||
): List<OCFileEntity>
|
||||
|
||||
@Query(SELECT_FILES_WHERE_LAST_USAGE_IS_OLDER_THAN_GIVEN_TIME)
|
||||
fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFileEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertOrIgnore(ocFileEntity: OCFileEntity): Long
|
||||
|
||||
@Update
|
||||
fun updateFile(ocFileEntity: OCFileEntity)
|
||||
|
||||
@Upsert
|
||||
fun upsert(ocFileEntity: OCFileEntity)
|
||||
|
||||
@Transaction
|
||||
fun updateSyncStatusForFile(id: Long, workerUuid: UUID?) {
|
||||
val fileWithSyncInfoEntity = getFileWithSyncInfoById(id)
|
||||
|
||||
if ((fileWithSyncInfoEntity?.file?.parentId != ROOT_PARENT_ID) &&
|
||||
((workerUuid == null) != (fileWithSyncInfoEntity?.fileSync?.downloadWorkerUuid == null))
|
||||
) {
|
||||
val fileSyncEntity = if (workerUuid == null) {
|
||||
OCFileSyncEntity(
|
||||
fileId = id,
|
||||
uploadWorkerUuid = null,
|
||||
downloadWorkerUuid = null,
|
||||
isSynchronizing = false
|
||||
)
|
||||
} else {
|
||||
OCFileSyncEntity(
|
||||
fileId = id,
|
||||
uploadWorkerUuid = null,
|
||||
downloadWorkerUuid = workerUuid,
|
||||
isSynchronizing = true
|
||||
)
|
||||
}
|
||||
insertOrReplaceFileSync(fileSyncEntity)
|
||||
|
||||
// Check if there is any more file synchronizing in this folder, in such case don't update parent's sync status
|
||||
var cleanSyncInParent = true
|
||||
if (workerUuid == null) {
|
||||
val folderContent = getFolderContentWithSyncInfo(fileWithSyncInfoEntity?.file?.parentId!!)
|
||||
var indexFileInFolder = 0
|
||||
while (cleanSyncInParent && indexFileInFolder < folderContent.size) {
|
||||
val child = folderContent[indexFileInFolder]
|
||||
if (child.fileSync?.isSynchronizing == true) {
|
||||
cleanSyncInParent = false
|
||||
}
|
||||
indexFileInFolder++
|
||||
}
|
||||
}
|
||||
if (workerUuid != null || cleanSyncInParent) {
|
||||
updateSyncStatusForFile(fileWithSyncInfoEntity?.file?.parentId!!, workerUuid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplaceFileSync(ocFileSyncEntity: OCFileSyncEntity): Long
|
||||
|
||||
/**
|
||||
* Make sure that the ids are set properly. We don't take care of conflicts and that stuff here.
|
||||
*
|
||||
* return folder content
|
||||
*/
|
||||
@Transaction
|
||||
fun insertFilesInFolderAndReturnTheFilesThatChanged(
|
||||
folder: OCFileEntity,
|
||||
folderContent: List<OCFileEntity>,
|
||||
): List<OCFileEntity> {
|
||||
var folderId = insertOrIgnore(folder)
|
||||
// If it was already in database
|
||||
if (folderId == -1L) {
|
||||
updateFile(folder)
|
||||
folderId = folder.id
|
||||
}
|
||||
|
||||
folderContent.forEach { fileToInsert ->
|
||||
upsert(fileToInsert.apply {
|
||||
parentId = folderId
|
||||
availableOfflineStatus = getNewAvailableOfflineStatus(folder.availableOfflineStatus, fileToInsert.availableOfflineStatus)
|
||||
})
|
||||
}
|
||||
val folderContentLocal = getFolderContent(folderId)
|
||||
|
||||
return folderContentLocal.filter { localFile ->
|
||||
folderContent.any { changedFile ->
|
||||
localFile.remoteId == changedFile.remoteId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
fun mergeRemoteAndLocalFile(
|
||||
ocFileEntity: OCFileEntity
|
||||
): Long {
|
||||
val localFile: OCFileEntity? = getFileByOwnerAndRemotePath(
|
||||
owner = ocFileEntity.owner,
|
||||
remotePath = ocFileEntity.remotePath,
|
||||
ocFileEntity.spaceId,
|
||||
)
|
||||
return if (localFile == null) {
|
||||
insertOrIgnore(ocFileEntity)
|
||||
} else {
|
||||
insertOrIgnore(ocFileEntity.copy(
|
||||
parentId = localFile.parentId,
|
||||
lastSyncDateForData = localFile.lastSyncDateForData,
|
||||
modifiedAtLastSyncForData = localFile.modifiedAtLastSyncForData,
|
||||
storagePath = localFile.storagePath,
|
||||
treeEtag = localFile.treeEtag,
|
||||
etagInConflict = localFile.etagInConflict,
|
||||
availableOfflineStatus = localFile.availableOfflineStatus,
|
||||
lastUsage = localFile.lastUsage,
|
||||
).apply {
|
||||
id = localFile.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Transaction
|
||||
fun copy(
|
||||
sourceFile: OCFileEntity,
|
||||
targetFolder: OCFileEntity,
|
||||
finalRemotePath: String,
|
||||
remoteId: String?,
|
||||
replace: Boolean?,
|
||||
) {
|
||||
// 1. Update target size
|
||||
upsert(
|
||||
targetFolder.copy(
|
||||
length = targetFolder.length + sourceFile.length
|
||||
).apply { id = targetFolder.id }
|
||||
)
|
||||
|
||||
if (replace == true) {
|
||||
remoteId?.let { deleteFileByRemoteId(it) }
|
||||
}
|
||||
|
||||
// 2. Insert a new file with common attributes and retrieved remote id
|
||||
upsert(
|
||||
OCFileEntity(
|
||||
parentId = targetFolder.id,
|
||||
owner = targetFolder.owner,
|
||||
remotePath = finalRemotePath,
|
||||
remoteId = remoteId,
|
||||
length = sourceFile.length,
|
||||
modificationTimestamp = sourceFile.modificationTimestamp,
|
||||
mimeType = sourceFile.mimeType,
|
||||
name = null,
|
||||
needsToUpdateThumbnail = true,
|
||||
etag = "",
|
||||
creationTimestamp = null,
|
||||
permissions = null,
|
||||
treeEtag = "",
|
||||
availableOfflineStatus = NOT_AVAILABLE_OFFLINE.ordinal,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
fun moveFile(
|
||||
sourceFile: OCFileEntity,
|
||||
targetFolder: OCFileEntity,
|
||||
finalRemotePath: String,
|
||||
finalStoragePath: String
|
||||
) {
|
||||
// 1. Update target size
|
||||
upsert(
|
||||
targetFolder.copy(
|
||||
length = targetFolder.length + sourceFile.length
|
||||
).apply { id = targetFolder.id }
|
||||
)
|
||||
|
||||
// 2. Update source
|
||||
if (sourceFile.isFolder) {
|
||||
// Update remote path and storage path when moving a folder
|
||||
moveFolder(
|
||||
sourceFolder = sourceFile,
|
||||
targetFolder = targetFolder,
|
||||
targetRemotePath = finalRemotePath,
|
||||
targetStoragePath = finalStoragePath
|
||||
)
|
||||
} else {
|
||||
// Update remote path, storage path, parent file when moving a file
|
||||
moveSingleFile(
|
||||
sourceFile = sourceFile,
|
||||
targetFolder = targetFolder,
|
||||
finalRemotePath = finalRemotePath,
|
||||
finalStoragePath = sourceFile.storagePath?.let { finalStoragePath }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Query(DELETE_FILE_WITH_ID)
|
||||
fun deleteFileById(id: Long)
|
||||
|
||||
@Query(DELETE_FILE_WITH_REMOTE_ID)
|
||||
fun deleteFileByRemoteId(remoteId: String)
|
||||
|
||||
@Query(UPDATE_FILES_STORAGE_DIRECTORY)
|
||||
fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String)
|
||||
|
||||
@Transaction
|
||||
fun updateAvailableOfflineStatusForFile(ocFile: OCFile, newAvailableOfflineStatus: Int) {
|
||||
if (ocFile.isFolder) {
|
||||
updateFolderWithNewAvailableOfflineStatus(ocFile.id!!, newAvailableOfflineStatus)
|
||||
} else {
|
||||
updateFileWithAvailableOfflineStatus(ocFile.id!!, newAvailableOfflineStatus)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFolderWithNewAvailableOfflineStatus(ocFolderId: Long, newAvailableOfflineStatus: Int) {
|
||||
updateFileWithAvailableOfflineStatus(ocFolderId, newAvailableOfflineStatus)
|
||||
|
||||
val newStatusForChildren = if (newAvailableOfflineStatus == NOT_AVAILABLE_OFFLINE.ordinal) {
|
||||
NOT_AVAILABLE_OFFLINE.ordinal
|
||||
} else {
|
||||
AVAILABLE_OFFLINE_PARENT.ordinal
|
||||
}
|
||||
val folderContent = getFolderContent(ocFolderId)
|
||||
folderContent.forEach { folderChild ->
|
||||
if (folderChild.isFolder) {
|
||||
updateFolderWithNewAvailableOfflineStatus(folderChild.id, newStatusForChildren)
|
||||
} else {
|
||||
updateFileWithAvailableOfflineStatus(folderChild.id, newStatusForChildren)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Query(UPDATE_FILE_WITH_NEW_AVAILABLE_OFFLINE_STATUS)
|
||||
fun updateFileWithAvailableOfflineStatus(id: Long, availableOfflineStatus: Int)
|
||||
|
||||
@Query(UPDATE_FILE_WITH_LAST_USAGE)
|
||||
fun updateFileWithLastUsage(id: Long, lastUsage: Long?)
|
||||
|
||||
@Transaction
|
||||
fun updateConflictStatusForFile(id: Long, eTagInConflict: String?) {
|
||||
val fileEntity = getFileById(id)
|
||||
|
||||
if (fileEntity?.parentId != ROOT_PARENT_ID) {
|
||||
updateFileWithConflictStatus(id, eTagInConflict)
|
||||
|
||||
// Check if there is any more file with conflicts in this folder, in such case don't update parent's conflict status
|
||||
var cleanConflictInParent = true
|
||||
if (eTagInConflict == null) {
|
||||
val folderContent = getFolderContent(fileEntity?.parentId!!)
|
||||
var indexFileInFolder = 0
|
||||
while (cleanConflictInParent && indexFileInFolder < folderContent.size) {
|
||||
val child = folderContent[indexFileInFolder]
|
||||
if (child.etagInConflict != null) {
|
||||
cleanConflictInParent = false
|
||||
}
|
||||
indexFileInFolder++
|
||||
}
|
||||
}
|
||||
if (eTagInConflict != null || cleanConflictInParent) {
|
||||
updateConflictStatusForFile(fileEntity?.parentId!!, eTagInConflict)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Query(UPDATE_FILE_WITH_NEW_CONFLICT_STATUS)
|
||||
fun updateFileWithConflictStatus(id: Long, eTagInConflict: String?)
|
||||
|
||||
@Query(DISABLE_THUMBNAILS_FOR_FILE)
|
||||
fun disableThumbnailsForFile(fileId: Long)
|
||||
|
||||
@Query(DELETE_FILES_FOR_ACCOUNT)
|
||||
fun deleteFilesForAccount(accountName: String)
|
||||
|
||||
private fun moveSingleFile(
|
||||
sourceFile: OCFileEntity,
|
||||
targetFolder: OCFileEntity,
|
||||
finalRemotePath: String,
|
||||
finalStoragePath: String?
|
||||
) {
|
||||
upsert(
|
||||
sourceFile.copy(
|
||||
parentId = targetFolder.id,
|
||||
remotePath = finalRemotePath,
|
||||
storagePath = finalStoragePath,
|
||||
availableOfflineStatus = getNewAvailableOfflineStatus(targetFolder.availableOfflineStatus, sourceFile.availableOfflineStatus)
|
||||
).apply { id = sourceFile.id }
|
||||
)
|
||||
}
|
||||
|
||||
private fun moveFolder(
|
||||
sourceFolder: OCFileEntity,
|
||||
targetFolder: OCFileEntity,
|
||||
targetRemotePath: String,
|
||||
targetStoragePath: String?
|
||||
) {
|
||||
// 1. Move the folder
|
||||
val folderRemotePath =
|
||||
targetRemotePath.trimEnd(separatorChar).plus(separatorChar)
|
||||
val folderStoragePath =
|
||||
targetStoragePath?.trimEnd(separatorChar)?.plus(separatorChar)
|
||||
|
||||
moveSingleFile(
|
||||
sourceFile = sourceFolder,
|
||||
targetFolder = targetFolder,
|
||||
finalRemotePath = folderRemotePath,
|
||||
finalStoragePath = sourceFolder.storagePath?.let { folderStoragePath }
|
||||
)
|
||||
|
||||
// 2. Move its content
|
||||
val folderContent = getFolderContent(sourceFolder.id)
|
||||
|
||||
folderContent.forEach { file ->
|
||||
val remotePathForChild = folderRemotePath.plus(file.name)
|
||||
val storagePathForChild = folderStoragePath?.plus(file.name)
|
||||
|
||||
if (file.isFolder) {
|
||||
moveFolder(
|
||||
sourceFolder = file,
|
||||
targetFolder = sourceFolder,
|
||||
targetRemotePath = remotePathForChild,
|
||||
targetStoragePath = storagePathForChild
|
||||
)
|
||||
} else {
|
||||
moveSingleFile(
|
||||
sourceFile = file,
|
||||
targetFolder = sourceFolder,
|
||||
finalRemotePath = remotePathForChild,
|
||||
finalStoragePath = storagePathForChild
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If folder is available offline, the child gets the AVAILABLE_OFFLINE_PARENT status
|
||||
* If child was available offline because of the previous parent, it won't be av offline anymore
|
||||
* Otherwise, keep the child available offline status
|
||||
*/
|
||||
private fun getNewAvailableOfflineStatus(
|
||||
parentFolderAvailableOfflineStatus: Int?,
|
||||
currentFileAvailableOfflineStatus: Int?,
|
||||
): Int =
|
||||
if ((parentFolderAvailableOfflineStatus != null) &&
|
||||
parentFolderAvailableOfflineStatus.isOneOf(AVAILABLE_OFFLINE.ordinal, AVAILABLE_OFFLINE_PARENT.ordinal)
|
||||
) {
|
||||
AVAILABLE_OFFLINE_PARENT.ordinal
|
||||
} else if (currentFileAvailableOfflineStatus == AVAILABLE_OFFLINE.ordinal) {
|
||||
AVAILABLE_OFFLINE.ordinal
|
||||
} else {
|
||||
NOT_AVAILABLE_OFFLINE.ordinal
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val SELECT_FILE_WITH_ID = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val SELECT_FILE_WITH_REMOTE_ID = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE remoteId = :remoteId
|
||||
"""
|
||||
|
||||
private const val SELECT_FILE_FROM_OWNER_WITH_REMOTE_PATH = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE owner = :owner AND remotePath = :remotePath AND spaceId IS :spaceId
|
||||
"""
|
||||
|
||||
private const val DELETE_FILE_WITH_ID = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE id = :id
|
||||
"""
|
||||
private const val DELETE_FILE_WITH_REMOTE_ID = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE remoteId = :remoteId
|
||||
"""
|
||||
|
||||
private const val SELECT_FOLDER_CONTENT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE parentId = :folderId
|
||||
"""
|
||||
|
||||
private const val SELECT_FILTERED_FOLDER_CONTENT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE parentId = :folderId AND remotePath LIKE '%' || :search || '%'
|
||||
"""
|
||||
|
||||
private const val SELECT_FILTERED_AVAILABLE_OFFLINE_FOLDER_CONTENT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE parentId = :folderId AND keepInSync = '1' AND remotePath LIKE '%' || :search || '%'
|
||||
"""
|
||||
|
||||
private const val SELECT_FILTERED_SHARED_BY_LINK_FOLDER_CONTENT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE parentId = :folderId AND remotePath LIKE '%' || :search || '%' AND sharedByLink LIKE '%1%'
|
||||
"""
|
||||
|
||||
private const val SELECT_FOLDER_BY_MIMETYPE = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE parentId = :folderId AND mimeType LIKE :mimeType || '%'
|
||||
"""
|
||||
|
||||
private const val SELECT_DOWNLOADED_FILES_FOR_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE owner = :accountOwner AND storagePath IS NOT NULL AND keepInSync = '0'
|
||||
"""
|
||||
|
||||
private const val SELECT_FILES_SHARED_BY_LINK = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE owner = :accountOwner AND sharedByLink LIKE '%1%'
|
||||
"""
|
||||
|
||||
private const val SELECT_FILES_AVAILABLE_OFFLINE_FROM_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE owner = :accountOwner AND keepInSync = '1'
|
||||
"""
|
||||
|
||||
private const val SELECT_FILES_AVAILABLE_OFFLINE_FROM_EVERY_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE keepInSync = '1'
|
||||
"""
|
||||
|
||||
private const val SELECT_FILES_WHERE_LAST_USAGE_IS_OLDER_THAN_GIVEN_TIME = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE lastUsage < (strftime('%s', 'now') * 1000 - :milliseconds)
|
||||
AND keepInSync = '0'
|
||||
"""
|
||||
|
||||
private const val UPDATE_FILE_WITH_NEW_AVAILABLE_OFFLINE_STATUS = """
|
||||
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
SET keepInSync = :availableOfflineStatus
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val UPDATE_FILE_WITH_NEW_CONFLICT_STATUS = """
|
||||
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
SET etagInConflict = :eTagInConflict
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val UPDATE_FILE_WITH_LAST_USAGE = """
|
||||
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
SET lastUsage = :lastUsage
|
||||
WHERE id = :id
|
||||
"""
|
||||
private const val DISABLE_THUMBNAILS_FOR_FILE = """
|
||||
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
SET needsToUpdateThumbnail = false
|
||||
WHERE id = :fileId
|
||||
"""
|
||||
|
||||
private const val UPDATE_FILES_STORAGE_DIRECTORY = """
|
||||
UPDATE ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
SET storagePath = `REPLACE`(storagePath, :oldDirectory, :newDirectory)
|
||||
WHERE storagePath IS NOT NULL
|
||||
"""
|
||||
|
||||
private const val DELETE_FILES_FOR_ACCOUNT = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME}
|
||||
WHERE owner = :accountName
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* qsfera 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.db
|
||||
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity
|
||||
|
||||
data class OCFileAndFileSync(
|
||||
@Embedded val file: OCFileEntity,
|
||||
@Relation(
|
||||
parentColumn = "id",
|
||||
entityColumn = "fileId"
|
||||
)
|
||||
val fileSync: OCFileSyncEntity?,
|
||||
@Relation(
|
||||
parentColumn = "spaceId",
|
||||
entityColumn = "space_id"
|
||||
)
|
||||
val space: SpacesEntity? = null,
|
||||
)
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.files.db
|
||||
|
||||
import android.database.Cursor
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_ACCOUNT_OWNER
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_CONTENT_LENGTH
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_CONTENT_TYPE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_CREATION
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_ETAG
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_ETAG_IN_CONFLICT
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_REMOTE_ETAG
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_IS_DOWNLOADING
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_KEEP_IN_SYNC
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_MODIFIED
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_OWNER
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PARENT
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PATH
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PERMISSIONS
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_PRIVATE_LINK
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_REMOTE_ID
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_SHARED_VIA_LINK
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_SHARED_WITH_SHAREE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_SPACE_ID
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_STORAGE_PATH
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_TREE_ETAG
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILE_UPDATE_THUMBNAIL
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta._ID
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID
|
||||
import eu.qsfera.android.domain.extensions.isOneOf
|
||||
import eu.qsfera.android.domain.files.model.MIME_DIR
|
||||
import eu.qsfera.android.domain.files.model.MIME_DIR_UNIX
|
||||
|
||||
@Entity(
|
||||
tableName = FILES_TABLE_NAME,
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = SpacesEntity::class,
|
||||
parentColumns = arrayOf(SPACES_ACCOUNT_NAME, SPACES_ID),
|
||||
childColumns = arrayOf(FILE_OWNER, FILE_SPACE_ID),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class OCFileEntity(
|
||||
var parentId: Long? = null,
|
||||
val owner: String,
|
||||
val remotePath: String,
|
||||
val remoteId: String?,
|
||||
val length: Long,
|
||||
val creationTimestamp: Long?,
|
||||
val modificationTimestamp: Long,
|
||||
val mimeType: String,
|
||||
val etag: String?,
|
||||
val remoteEtag: String? = null,
|
||||
val permissions: String?,
|
||||
val privateLink: String? = null,
|
||||
val storagePath: String? = null,
|
||||
var name: String? = null,
|
||||
val treeEtag: String? = null,
|
||||
@ColumnInfo(name = "keepInSync")
|
||||
var availableOfflineStatus: Int? = null,
|
||||
val lastSyncDateForData: Long? = null,
|
||||
val lastUsage: Long? = null,
|
||||
val fileShareViaLink: Int? = null,
|
||||
var needsToUpdateThumbnail: Boolean = false,
|
||||
val modifiedAtLastSyncForData: Long? = null,
|
||||
val etagInConflict: String? = null,
|
||||
val fileIsDownloading: Boolean? = null,
|
||||
val sharedWithSharee: Boolean? = false,
|
||||
var sharedByLink: Boolean = false,
|
||||
val spaceId: String? = null,
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
/**
|
||||
* Use this to find out if this file is a folder.
|
||||
*
|
||||
* @return true if it is a folder
|
||||
*/
|
||||
val isFolder
|
||||
get() = mimeType.isOneOf(MIME_DIR, MIME_DIR_UNIX)
|
||||
|
||||
companion object {
|
||||
fun fromCursor(cursor: Cursor): OCFileEntity =
|
||||
OCFileEntity(
|
||||
parentId = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_PARENT)),
|
||||
remotePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PATH)),
|
||||
owner = cursor.getString(cursor.getColumnIndexOrThrow(FILE_ACCOUNT_OWNER)),
|
||||
permissions = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PERMISSIONS)),
|
||||
remoteId = cursor.getString(cursor.getColumnIndexOrThrow(FILE_REMOTE_ID)),
|
||||
privateLink = cursor.getString(cursor.getColumnIndexOrThrow(FILE_PRIVATE_LINK)),
|
||||
creationTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_CREATION)),
|
||||
modificationTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_MODIFIED)),
|
||||
etag = cursor.getString(cursor.getColumnIndexOrThrow(FILE_ETAG)),
|
||||
remoteEtag = cursor.getString(cursor.getColumnIndexOrThrow(FILE_REMOTE_ETAG)),
|
||||
mimeType = cursor.getStringFromColumnOrEmpty(FILE_CONTENT_TYPE),
|
||||
length = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_CONTENT_LENGTH)),
|
||||
storagePath = cursor.getString(cursor.getColumnIndexOrThrow(FILE_STORAGE_PATH)),
|
||||
name = cursor.getString(cursor.getColumnIndexOrThrow(FILE_NAME)),
|
||||
treeEtag = cursor.getString(cursor.getColumnIndexOrThrow(FILE_TREE_ETAG)),
|
||||
lastSyncDateForData = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_LAST_SYNC_DATE_FOR_DATA)),
|
||||
availableOfflineStatus = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_KEEP_IN_SYNC)),
|
||||
fileShareViaLink = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_SHARED_VIA_LINK)),
|
||||
needsToUpdateThumbnail = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_UPDATE_THUMBNAIL)) == 1,
|
||||
modifiedAtLastSyncForData = cursor.getLong(cursor.getColumnIndexOrThrow(FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA)),
|
||||
etagInConflict = cursor.getString(cursor.getColumnIndexOrThrow(FILE_ETAG_IN_CONFLICT)),
|
||||
fileIsDownloading = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_IS_DOWNLOADING)) == 1,
|
||||
sharedWithSharee = cursor.getInt(cursor.getColumnIndexOrThrow(FILE_SHARED_WITH_SHAREE)) == 1
|
||||
).apply {
|
||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(_ID))
|
||||
}
|
||||
|
||||
private fun Cursor.getStringFromColumnOrEmpty(
|
||||
columnName: String
|
||||
): String = getColumnIndex(columnName).takeUnless { it < 0 }?.let { getString(it) }.orEmpty()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* qsfera 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILES_SYNC_TABLE_NAME
|
||||
import java.util.UUID
|
||||
|
||||
@Entity(
|
||||
tableName = FILES_SYNC_TABLE_NAME,
|
||||
foreignKeys = [ForeignKey(
|
||||
entity = OCFileEntity::class,
|
||||
parentColumns = arrayOf("id"),
|
||||
childColumns = arrayOf("fileId"),
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class OCFileSyncEntity(
|
||||
@PrimaryKey val fileId: Long,
|
||||
val uploadWorkerUuid: UUID?,
|
||||
val downloadWorkerUuid: UUID?,
|
||||
val isSynchronizing: Boolean
|
||||
)
|
||||
+619
@@ -0,0 +1,619 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Christian Schabesberger
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Manuel Plazas Palacio
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.files.repository
|
||||
|
||||
import eu.qsfera.android.data.files.datasources.LocalFileDataSource
|
||||
import eu.qsfera.android.data.files.datasources.RemoteFileDataSource
|
||||
import eu.qsfera.android.data.providers.LocalStorageProvider
|
||||
import eu.qsfera.android.data.spaces.datasources.LocalSpacesDataSource
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.AVAILABLE_OFFLINE_PARENT
|
||||
import eu.qsfera.android.domain.availableoffline.model.AvailableOfflineStatus.NOT_AVAILABLE_OFFLINE
|
||||
import eu.qsfera.android.domain.exceptions.ConflictException
|
||||
import eu.qsfera.android.domain.exceptions.FileAlreadyExistsException
|
||||
import eu.qsfera.android.domain.exceptions.FileNotFoundException
|
||||
import eu.qsfera.android.domain.files.FileRepository
|
||||
import eu.qsfera.android.domain.files.model.FileListOption
|
||||
import eu.qsfera.android.domain.files.model.MIME_DIR
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.files.model.OCFile.Companion.PATH_SEPARATOR
|
||||
import eu.qsfera.android.domain.files.model.OCFile.Companion.ROOT_PATH
|
||||
import eu.qsfera.android.domain.files.model.OCFileWithSyncInfo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
class OCFileRepository(
|
||||
private val localFileDataSource: LocalFileDataSource,
|
||||
private val remoteFileDataSource: RemoteFileDataSource,
|
||||
private val localSpacesDataSource: LocalSpacesDataSource,
|
||||
private val localStorageProvider: LocalStorageProvider,
|
||||
) : FileRepository {
|
||||
override fun createFolder(
|
||||
remotePath: String,
|
||||
parentFolder: OCFile,
|
||||
) {
|
||||
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(parentFolder.spaceId, parentFolder.owner)
|
||||
|
||||
remoteFileDataSource.createFolder(
|
||||
remotePath = remotePath,
|
||||
createFullPath = false,
|
||||
isChunksFolder = false,
|
||||
accountName = parentFolder.owner,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
).also {
|
||||
localFileDataSource.saveFilesInFolderAndReturnTheFilesThatChanged(
|
||||
folder = parentFolder,
|
||||
listOfFiles = listOf(
|
||||
OCFile(
|
||||
remotePath = remotePath,
|
||||
owner = parentFolder.owner,
|
||||
modificationTimestamp = System.currentTimeMillis(),
|
||||
length = 0,
|
||||
mimeType = MIME_DIR,
|
||||
spaceId = parentFolder.spaceId,
|
||||
permissions = "CK" // To be able to write inside a folder before the fetch is done
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun copyFile(listOfFilesToCopy: List<OCFile>, targetFolder: OCFile, replace: List<Boolean?>, isUserLogged: Boolean): List<OCFile> {
|
||||
val sourceSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(listOfFilesToCopy[0].spaceId, listOfFilesToCopy[0].owner)
|
||||
val targetSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFolder.spaceId, targetFolder.owner)
|
||||
val filesNeedAction = mutableListOf<OCFile>()
|
||||
|
||||
listOfFilesToCopy.forEachIndexed forEach@{ position, ocFile ->
|
||||
|
||||
// 1. Get the final remote path for this file.
|
||||
val expectedRemotePath: String = targetFolder.remotePath + ocFile.fileName
|
||||
|
||||
val finalRemotePath: String? =
|
||||
getFinalRemotePath(
|
||||
replace = replace,
|
||||
expectedRemotePath = expectedRemotePath,
|
||||
targetFolder = targetFolder,
|
||||
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
filesNeedsAction = filesNeedAction,
|
||||
ocFile = ocFile,
|
||||
position = position,
|
||||
isUserLogged = isUserLogged,
|
||||
)
|
||||
if (finalRemotePath != null && (replace.isEmpty() || replace[position] != null)) {
|
||||
// 2. Try to copy files in server
|
||||
val remoteId = try {
|
||||
remoteFileDataSource.copyFile(
|
||||
sourceRemotePath = ocFile.remotePath,
|
||||
targetRemotePath = finalRemotePath,
|
||||
accountName = ocFile.owner,
|
||||
sourceSpaceWebDavUrl = sourceSpaceWebDavUrl,
|
||||
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
replace = if (replace.isEmpty()) false else replace[position]!!,
|
||||
)
|
||||
} catch (targetNodeDoesNotExist: ConflictException) {
|
||||
// Target node does not exist anymore. Remove target folder from database and local storage and return
|
||||
deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false)
|
||||
throw targetNodeDoesNotExist
|
||||
} catch (sourceFileDoesNotExist: FileNotFoundException) {
|
||||
// Source file does not exist anymore. Remove file from database and local storage and continue
|
||||
if (ocFile.isFolder) {
|
||||
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false)
|
||||
} else {
|
||||
deleteLocalFile(
|
||||
ocFile = ocFile,
|
||||
onlyFromLocalStorage = false
|
||||
)
|
||||
}
|
||||
if (listOfFilesToCopy.size == 1) {
|
||||
throw sourceFileDoesNotExist
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Update database with latest changes
|
||||
remoteId?.let {
|
||||
localFileDataSource.copyFile(
|
||||
sourceFile = ocFile,
|
||||
targetFolder = targetFolder,
|
||||
finalRemotePath = finalRemotePath,
|
||||
remoteId = it,
|
||||
replace = if (replace.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
replace[position]
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filesNeedAction
|
||||
}
|
||||
|
||||
override fun getFileById(fileId: Long): OCFile? =
|
||||
localFileDataSource.getFileById(fileId)
|
||||
|
||||
override fun getFileByIdAsFlow(fileId: Long): Flow<OCFile?> =
|
||||
localFileDataSource.getFileByIdAsFlow(fileId)
|
||||
|
||||
override fun getFileWithSyncInfoByIdAsFlow(fileId: Long): Flow<OCFileWithSyncInfo?> =
|
||||
localFileDataSource.getFileWithSyncInfoByIdAsFlow(fileId)
|
||||
|
||||
override fun getFileByRemotePath(remotePath: String, owner: String, spaceId: String?): OCFile? =
|
||||
localFileDataSource.getFileByRemotePath(remotePath, owner, spaceId)
|
||||
|
||||
override fun getFileFromRemoteId(fileId: String, accountName: String): OCFile? {
|
||||
val metaFile = remoteFileDataSource.getMetaFile(fileId, accountName)
|
||||
val remotePath = metaFile.path!!
|
||||
|
||||
val splitPath = remotePath.split(PATH_SEPARATOR)
|
||||
var containerFolder = listOf<OCFile>()
|
||||
for (i in 0..splitPath.size - 2) {
|
||||
var path = splitPath[0]
|
||||
for (j in 1..i) {
|
||||
path += "$PATH_SEPARATOR${splitPath[j]}"
|
||||
}
|
||||
containerFolder = refreshFolder(path, accountName, metaFile.spaceId)
|
||||
}
|
||||
refreshFolder(remotePath, accountName, metaFile.spaceId)
|
||||
return if (remotePath == ROOT_PATH) {
|
||||
getFileByRemotePath(remotePath, accountName, metaFile.spaceId)
|
||||
} else {
|
||||
containerFolder.find { file ->
|
||||
if (file.isFolder) {
|
||||
file.remotePath.dropLast(1)
|
||||
} else {
|
||||
file.remotePath
|
||||
} == remotePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPersonalRootFolderForAccount(owner: String): OCFile {
|
||||
val personalSpace = localSpacesDataSource.getPersonalSpaceForAccount(owner)
|
||||
if (personalSpace == null) {
|
||||
val legacyRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = null)
|
||||
try {
|
||||
return legacyRootFolder ?: throw IllegalStateException("LegacyRootFolder not found")
|
||||
} catch (e: IllegalStateException) {
|
||||
Timber.i("There was an error: $e")
|
||||
}
|
||||
}
|
||||
// To do: Retrieving the root folders should return a non nullable. If they don't exist yet, they are created and returned. Remove nullability
|
||||
val personalRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = personalSpace?.root?.id)
|
||||
return personalRootFolder!!
|
||||
}
|
||||
|
||||
override fun getSharesRootFolderForAccount(owner: String): OCFile? {
|
||||
val sharesSpaces = localSpacesDataSource.getSharesSpaceForAccount(owner) ?: return null
|
||||
|
||||
val personalRootFolder = localFileDataSource.getFileByRemotePath(remotePath = ROOT_PATH, owner = owner, spaceId = sharesSpaces.root.id)
|
||||
return personalRootFolder!!
|
||||
}
|
||||
|
||||
override fun getSearchFolderContent(fileListOption: FileListOption, folderId: Long, search: String): List<OCFile> =
|
||||
when (fileListOption) {
|
||||
FileListOption.ALL_FILES -> localFileDataSource.getSearchFolderContent(folderId, search)
|
||||
FileListOption.SPACES_LIST -> emptyList()
|
||||
FileListOption.AV_OFFLINE -> localFileDataSource.getSearchAvailableOfflineFolderContent(folderId, search)
|
||||
FileListOption.SHARED_BY_LINK -> localFileDataSource.getSearchSharedByLinkFolderContent(folderId, search)
|
||||
}
|
||||
|
||||
override fun getFolderContent(folderId: Long): List<OCFile> =
|
||||
localFileDataSource.getFolderContent(folderId)
|
||||
|
||||
override fun getFolderContentWithSyncInfoAsFlow(folderId: Long): Flow<List<OCFileWithSyncInfo>> =
|
||||
localFileDataSource.getFolderContentWithSyncInfoAsFlow(folderId)
|
||||
|
||||
override fun getFolderImages(folderId: Long): List<OCFile> =
|
||||
localFileDataSource.getFolderImages(folderId)
|
||||
|
||||
override fun getSharedByLinkWithSyncInfoForAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
|
||||
localFileDataSource.getSharedByLinkWithSyncInfoForAccountAsFlow(owner)
|
||||
|
||||
override fun getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner: String): Flow<List<OCFileWithSyncInfo>> =
|
||||
localFileDataSource.getFilesWithSyncInfoAvailableOfflineFromAccountAsFlow(owner)
|
||||
|
||||
override fun getFilesAvailableOfflineFromAccount(owner: String): List<OCFile> =
|
||||
localFileDataSource.getFilesAvailableOfflineFromAccount(owner)
|
||||
|
||||
override fun getFilesAvailableOfflineFromEveryAccount(): List<OCFile> =
|
||||
localFileDataSource.getFilesAvailableOfflineFromEveryAccount()
|
||||
|
||||
override fun getDownloadedFilesForAccount(owner: String): List<OCFile> = localFileDataSource.getDownloadedFilesForAccount(owner)
|
||||
|
||||
override fun getFilesWithLastUsageOlderThanGivenTime(milliseconds: Long): List<OCFile> =
|
||||
localFileDataSource.getFilesWithLastUsageOlderThanGivenTime(milliseconds)
|
||||
|
||||
override fun moveFile(listOfFilesToMove: List<OCFile>, targetFolder: OCFile, replace: List<Boolean?>, isUserLogged: Boolean): List<OCFile> {
|
||||
val targetSpaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(targetFolder.spaceId, targetFolder.owner)
|
||||
val filesNeedsAction = mutableListOf<OCFile>()
|
||||
|
||||
|
||||
listOfFilesToMove.forEachIndexed forEach@{ position, ocFile ->
|
||||
|
||||
// 1. Get the final remote path for this file.
|
||||
val expectedRemotePath: String = targetFolder.remotePath + ocFile.fileName
|
||||
val finalRemotePath: String? =
|
||||
getFinalRemotePath(
|
||||
replace = replace,
|
||||
expectedRemotePath = expectedRemotePath,
|
||||
targetFolder = targetFolder,
|
||||
targetSpaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
filesNeedsAction = filesNeedsAction,
|
||||
ocFile = ocFile,
|
||||
position = position,
|
||||
isUserLogged = isUserLogged,
|
||||
)
|
||||
|
||||
if (finalRemotePath != null && (replace.isEmpty() || replace[position] != null)) {
|
||||
val finalStoragePath: String = localStorageProvider.getDefaultSavePathFor(targetFolder.owner, finalRemotePath, targetFolder.spaceId)
|
||||
|
||||
// 2. Try to move files in server
|
||||
try {
|
||||
remoteFileDataSource.moveFile(
|
||||
sourceRemotePath = ocFile.remotePath,
|
||||
targetRemotePath = finalRemotePath,
|
||||
accountName = ocFile.owner,
|
||||
spaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
replace = if (replace.isEmpty()) false else replace[position]!!,
|
||||
)
|
||||
} catch (targetNodeDoesNotExist: ConflictException) {
|
||||
// Target node does not exist anymore. Remove target folder from database and local storage and return
|
||||
deleteLocalFolderRecursively(ocFile = targetFolder, onlyFromLocalStorage = false)
|
||||
throw targetNodeDoesNotExist
|
||||
} catch (sourceFileDoesNotExist: FileNotFoundException) {
|
||||
// Source file does not exist anymore. Remove file from database and local storage and continue
|
||||
if (ocFile.isFolder) {
|
||||
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false)
|
||||
} else {
|
||||
deleteLocalFile(
|
||||
ocFile = ocFile,
|
||||
onlyFromLocalStorage = false
|
||||
)
|
||||
}
|
||||
if (listOfFilesToMove.size == 1) {
|
||||
throw sourceFileDoesNotExist
|
||||
} else {
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clean conflict in old location if there was a conflict
|
||||
ocFile.etagInConflict?.let {
|
||||
localFileDataSource.cleanConflict(ocFile.id!!)
|
||||
}
|
||||
|
||||
// 4. Update database with latest changes
|
||||
localFileDataSource.moveFile(
|
||||
sourceFile = ocFile,
|
||||
targetFolder = targetFolder,
|
||||
finalRemotePath = finalRemotePath,
|
||||
finalStoragePath = finalStoragePath
|
||||
)
|
||||
|
||||
// 5. Save conflict in new location if there was conflict
|
||||
ocFile.etagInConflict?.let {
|
||||
localFileDataSource.saveConflict(ocFile.id!!, it)
|
||||
}
|
||||
|
||||
// 6. Update local storage
|
||||
localStorageProvider.moveLocalFile(ocFile, finalStoragePath)
|
||||
}
|
||||
}
|
||||
return filesNeedsAction
|
||||
}
|
||||
|
||||
override fun readFile(remotePath: String, accountName: String, spaceId: String?): OCFile {
|
||||
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(spaceId, accountName)
|
||||
|
||||
return remoteFileDataSource.readFile(remotePath, accountName, spaceWebDavUrl).copy(spaceId = spaceId)
|
||||
}
|
||||
|
||||
override fun refreshFolder(
|
||||
remotePath: String,
|
||||
accountName: String,
|
||||
spaceId: String?,
|
||||
isActionSetFolderAvailableOfflineOrSynchronize: Boolean,
|
||||
): List<OCFile> {
|
||||
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(spaceId, accountName)
|
||||
|
||||
// Retrieve remote folder data
|
||||
val fetchFolderResult = remoteFileDataSource.refreshFolder(remotePath, accountName, spaceWebDavUrl).map {
|
||||
it.copy(spaceId = spaceId)
|
||||
}
|
||||
val remoteFolder = fetchFolderResult.first()
|
||||
val remoteFolderContent = fetchFolderResult.drop(1).distinctBy { it.remotePath }
|
||||
|
||||
// Final content for this folder, we will update the folder content all together
|
||||
val folderContentUpdated = mutableListOf<OCFile>()
|
||||
|
||||
// Check if the folder already exists in database.
|
||||
val localFolderByRemotePath: OCFile? =
|
||||
localFileDataSource.getFileByRemotePath(remotePath = remoteFolder.remotePath, owner = remoteFolder.owner, spaceId = spaceId)
|
||||
|
||||
// If folder doesn't exists in database, insert everything. Easy path
|
||||
if (localFolderByRemotePath == null) {
|
||||
folderContentUpdated.addAll(remoteFolderContent.map { it.apply { needsToUpdateThumbnail = !it.isFolder } })
|
||||
} else {
|
||||
// Keep the current local properties or we will miss relevant things.
|
||||
remoteFolder.copyLocalPropertiesFrom(localFolderByRemotePath)
|
||||
|
||||
// Folder already exists in database, get database content to update files accordingly
|
||||
val localFolderContent = localFileDataSource.getFolderContent(folderId = localFolderByRemotePath.id!!)
|
||||
|
||||
val localFilesMap = localFolderContent.associateBy { localFile -> localFile.remoteId ?: localFile.remotePath }.toMutableMap()
|
||||
|
||||
// Loop to sync every child
|
||||
remoteFolderContent.forEach { remoteChild ->
|
||||
// Let's try with remote path if the file does not have remote id yet
|
||||
val localChildToSync = localFilesMap.remove(remoteChild.remoteId) ?: localFilesMap.remove(remoteChild.remotePath)
|
||||
|
||||
// If local child does not exists, just insert the new one.
|
||||
if (localChildToSync == null) {
|
||||
folderContentUpdated.add(
|
||||
remoteChild.apply {
|
||||
parentId = localFolderByRemotePath.id
|
||||
needsToUpdateThumbnail = !remoteChild.isFolder
|
||||
// remote eTag will not be set unless file CONTENTS are synchronized
|
||||
etag = ""
|
||||
availableOfflineStatus =
|
||||
if (remoteFolder.isAvailableOffline) AVAILABLE_OFFLINE_PARENT else NOT_AVAILABLE_OFFLINE
|
||||
|
||||
})
|
||||
} else if (localChildToSync.etag != remoteChild.etag ||
|
||||
localChildToSync.localModificationTimestamp > remoteChild.lastSyncDateForData!! ||
|
||||
isActionSetFolderAvailableOfflineOrSynchronize
|
||||
) {
|
||||
// File exists in the database, we need to check several stuff.
|
||||
folderContentUpdated.add(
|
||||
remoteChild.apply {
|
||||
copyLocalPropertiesFrom(localChildToSync)
|
||||
// DO NOT update etag till contents are synced.
|
||||
etag = localChildToSync.etag
|
||||
needsToUpdateThumbnail =
|
||||
(!remoteChild.isFolder && remoteChild.modificationTimestamp != localChildToSync.modificationTimestamp) ||
|
||||
localChildToSync.needsToUpdateThumbnail
|
||||
// Probably not needed, if the child was already in the database, the av offline status should be also there
|
||||
if (remoteFolder.isAvailableOffline) {
|
||||
availableOfflineStatus = AVAILABLE_OFFLINE_PARENT
|
||||
}
|
||||
// Fix: What about renames? Need to fix storage path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining items should be removed from the database and local storage. They do not exists in remote anymore.
|
||||
localFilesMap.map { it.value }.forEach { ocFile ->
|
||||
ocFile.etagInConflict?.let {
|
||||
localFileDataSource.cleanConflict(ocFile.id!!)
|
||||
}
|
||||
if (ocFile.isFolder) {
|
||||
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = false)
|
||||
} else {
|
||||
deleteLocalFile(ocFile = ocFile, onlyFromLocalStorage = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val anyConflictInThisFolder = folderContentUpdated.any { it.etagInConflict != null }
|
||||
|
||||
if (!anyConflictInThisFolder) {
|
||||
remoteFolder.etagInConflict = null
|
||||
}
|
||||
|
||||
return localFileDataSource.saveFilesInFolderAndReturnTheFilesThatChanged(
|
||||
folder = remoteFolder,
|
||||
listOfFiles = folderContentUpdated
|
||||
)
|
||||
}
|
||||
|
||||
override fun deleteFiles(listOfFilesToDelete: List<OCFile>, removeOnlyLocalCopy: Boolean) {
|
||||
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(
|
||||
spaceId = listOfFilesToDelete.first().spaceId,
|
||||
accountName = listOfFilesToDelete.first().owner,
|
||||
)
|
||||
|
||||
listOfFilesToDelete.forEach { ocFile ->
|
||||
if (!removeOnlyLocalCopy) {
|
||||
try {
|
||||
remoteFileDataSource.deleteFile(
|
||||
remotePath = ocFile.remotePath,
|
||||
accountName = ocFile.owner,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
} catch (fileNotFoundException: FileNotFoundException) {
|
||||
Timber.i(fileNotFoundException, "File ${ocFile.fileName} was not found in server. Let's remove it from local storage")
|
||||
}
|
||||
}
|
||||
ocFile.etagInConflict?.let {
|
||||
localFileDataSource.cleanConflict(ocFile.id!!)
|
||||
}
|
||||
if (ocFile.isFolder) {
|
||||
deleteLocalFolderRecursively(ocFile = ocFile, onlyFromLocalStorage = removeOnlyLocalCopy)
|
||||
} else {
|
||||
deleteLocalFile(ocFile = ocFile, onlyFromLocalStorage = removeOnlyLocalCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun renameFile(ocFile: OCFile, newName: String) {
|
||||
// 1. Compose new remote path
|
||||
val newRemotePath = localStorageProvider.getExpectedRemotePath(
|
||||
remotePath = ocFile.remotePath,
|
||||
newName = newName,
|
||||
isFolder = ocFile.isFolder
|
||||
)
|
||||
|
||||
// 2. Check if file already exists in database
|
||||
if (localFileDataSource.getFileByRemotePath(newRemotePath, ocFile.owner, ocFile.spaceId) != null) {
|
||||
throw FileAlreadyExistsException()
|
||||
}
|
||||
|
||||
// 3. Retrieve the specific web dav url in case there is one.
|
||||
val spaceWebDavUrl = localSpacesDataSource.getWebDavUrlForSpace(
|
||||
spaceId = ocFile.spaceId,
|
||||
accountName = ocFile.owner,
|
||||
)
|
||||
|
||||
// 4. Perform remote operation
|
||||
remoteFileDataSource.renameFile(
|
||||
oldName = ocFile.fileName,
|
||||
oldRemotePath = ocFile.remotePath,
|
||||
newName = newName,
|
||||
isFolder = ocFile.isFolder,
|
||||
accountName = ocFile.owner,
|
||||
spaceWebDavUrl = spaceWebDavUrl,
|
||||
)
|
||||
|
||||
// 5. Save new remote path in the local database
|
||||
localFileDataSource.renameFile(
|
||||
fileToRename = ocFile,
|
||||
finalRemotePath = newRemotePath,
|
||||
finalStoragePath = localStorageProvider.getDefaultSavePathFor(ocFile.owner, newRemotePath, ocFile.spaceId)
|
||||
)
|
||||
|
||||
// 6. Update local storage
|
||||
localStorageProvider.moveLocalFile(
|
||||
ocFile = ocFile,
|
||||
finalStoragePath = localStorageProvider.getDefaultSavePathFor(ocFile.owner, newRemotePath, ocFile.spaceId)
|
||||
)
|
||||
}
|
||||
|
||||
override fun saveFile(file: OCFile) {
|
||||
localFileDataSource.saveFile(file)
|
||||
}
|
||||
|
||||
override fun saveConflict(fileId: Long, eTagInConflict: String) {
|
||||
localFileDataSource.saveConflict(fileId, eTagInConflict)
|
||||
}
|
||||
|
||||
override fun cleanConflict(fileId: Long) {
|
||||
localFileDataSource.cleanConflict(fileId)
|
||||
}
|
||||
|
||||
override fun disableThumbnailsForFile(fileId: Long) {
|
||||
localFileDataSource.disableThumbnailsForFile(fileId)
|
||||
}
|
||||
|
||||
override fun updateFileWithNewAvailableOfflineStatus(ocFile: OCFile, newAvailableOfflineStatus: AvailableOfflineStatus) {
|
||||
localFileDataSource.updateAvailableOfflineStatusForFile(ocFile, newAvailableOfflineStatus)
|
||||
}
|
||||
|
||||
override fun updateFileWithLastUsage(fileId: Long, lastUsage: Long?) {
|
||||
localFileDataSource.updateFileWithLastUsage(fileId, lastUsage)
|
||||
}
|
||||
|
||||
override fun updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory: String, newDirectory: String) {
|
||||
localFileDataSource.updateDownloadedFilesStorageDirectoryInStoragePath(oldDirectory, newDirectory)
|
||||
}
|
||||
|
||||
override fun saveDownloadWorkerUuid(fileId: Long, workerUuid: UUID) {
|
||||
localFileDataSource.saveDownloadWorkerUuid(fileId, workerUuid)
|
||||
}
|
||||
|
||||
override fun cleanWorkersUuid(fileId: Long) {
|
||||
localFileDataSource.cleanWorkersUuid(fileId)
|
||||
}
|
||||
|
||||
private fun getFinalRemotePath(
|
||||
replace: List<Boolean?>,
|
||||
expectedRemotePath: String,
|
||||
targetFolder: OCFile,
|
||||
targetSpaceWebDavUrl: String?,
|
||||
filesNeedsAction: MutableList<OCFile>,
|
||||
ocFile: OCFile,
|
||||
position: Int,
|
||||
isUserLogged: Boolean,
|
||||
) =
|
||||
if (replace.isEmpty()) {
|
||||
val pathExists = remoteFileDataSource.checkPathExistence(
|
||||
path = expectedRemotePath,
|
||||
isUserLogged = isUserLogged,
|
||||
accountName = targetFolder.owner,
|
||||
spaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
)
|
||||
if (pathExists) {
|
||||
filesNeedsAction.add(ocFile)
|
||||
null
|
||||
} else {
|
||||
if (ocFile.isFolder) expectedRemotePath.plus(File.separator) else expectedRemotePath
|
||||
}
|
||||
} else {
|
||||
if (replace[position] == true) {
|
||||
if (ocFile.isFolder) expectedRemotePath.plus(File.separator) else expectedRemotePath
|
||||
} else if (replace[position] == false) {
|
||||
remoteFileDataSource.getAvailableRemotePath(
|
||||
remotePath = expectedRemotePath,
|
||||
accountName = targetFolder.owner,
|
||||
spaceWebDavUrl = targetSpaceWebDavUrl,
|
||||
isUserLogged = isUserLogged,
|
||||
).let {
|
||||
if (ocFile.isFolder) it.plus(File.separator) else it
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteLocalFolderRecursively(ocFile: OCFile, onlyFromLocalStorage: Boolean) {
|
||||
val folderContent = localFileDataSource.getFolderContent(ocFile.id!!)
|
||||
|
||||
// 1. Remove folder content recursively
|
||||
folderContent.forEach { file ->
|
||||
// The condition will not be met when onlyFromLocalStorage is true and the file is of type available offline
|
||||
if (!(onlyFromLocalStorage && file.isAvailableOffline)) {
|
||||
if (file.isFolder) {
|
||||
deleteLocalFolderRecursively(ocFile = file, onlyFromLocalStorage = onlyFromLocalStorage)
|
||||
} else {
|
||||
deleteLocalFile(ocFile = file, onlyFromLocalStorage = onlyFromLocalStorage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Remove the folder itself if it has no files
|
||||
deleteLocalFolderIfItHasNoFilesInside(ocFolder = ocFile, onlyFromLocalStorage = onlyFromLocalStorage)
|
||||
}
|
||||
|
||||
private fun deleteLocalFolderIfItHasNoFilesInside(ocFolder: OCFile, onlyFromLocalStorage: Boolean) {
|
||||
localStorageProvider.deleteLocalFolderIfItHasNoFilesInside(ocFolder = ocFolder)
|
||||
deleteOrResetFileFromDatabase(ocFolder, onlyFromLocalStorage)
|
||||
}
|
||||
|
||||
private fun deleteLocalFile(ocFile: OCFile, onlyFromLocalStorage: Boolean) {
|
||||
localStorageProvider.deleteLocalFile(ocFile)
|
||||
deleteOrResetFileFromDatabase(ocFile, onlyFromLocalStorage)
|
||||
}
|
||||
|
||||
private fun deleteOrResetFileFromDatabase(ocFile: OCFile, onlyFromLocalStorage: Boolean) {
|
||||
if (onlyFromLocalStorage) {
|
||||
localFileDataSource.saveFile(ocFile.copy(storagePath = null, etagInConflict = null, lastUsage = null, etag = null))
|
||||
} else {
|
||||
localFileDataSource.deleteFile(ocFile.id!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.folderbackup.datasources
|
||||
|
||||
import eu.qsfera.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalFolderBackupDataSource {
|
||||
fun getAutomaticUploadsConfiguration(): AutomaticUploadsConfiguration?
|
||||
|
||||
fun getFolderBackupConfigurationByNameAsFlow(name: String): Flow<FolderBackUpConfiguration?>
|
||||
|
||||
fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration)
|
||||
|
||||
fun resetFolderBackupConfigurationByName(name: String)
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.folderbackup.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.folderbackup.datasources.LocalFolderBackupDataSource
|
||||
import eu.qsfera.android.data.folderbackup.db.FolderBackUpEntity
|
||||
import eu.qsfera.android.data.folderbackup.db.FolderBackupDao
|
||||
import eu.qsfera.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.videoUploadsName
|
||||
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class OCLocalFolderBackupDataSource(
|
||||
private val folderBackupDao: FolderBackupDao,
|
||||
) : LocalFolderBackupDataSource {
|
||||
|
||||
override fun getAutomaticUploadsConfiguration(): AutomaticUploadsConfiguration? {
|
||||
val pictureUploadsConfiguration = folderBackupDao.getFolderBackUpConfigurationByName(pictureUploadsName)
|
||||
val videoUploadsConfiguration = folderBackupDao.getFolderBackUpConfigurationByName(videoUploadsName)
|
||||
|
||||
if (pictureUploadsConfiguration == null && videoUploadsConfiguration == null) return null
|
||||
|
||||
return AutomaticUploadsConfiguration(
|
||||
pictureUploadsConfiguration = pictureUploadsConfiguration?.toModel(),
|
||||
videoUploadsConfiguration = videoUploadsConfiguration?.toModel(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFolderBackupConfigurationByNameAsFlow(name: String): Flow<FolderBackUpConfiguration?> =
|
||||
folderBackupDao.getFolderBackUpConfigurationByNameAsFlow(name = name).map { it?.toModel() }
|
||||
|
||||
override fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) {
|
||||
folderBackupDao.update(folderBackUpConfiguration.toEntity())
|
||||
}
|
||||
|
||||
override fun resetFolderBackupConfigurationByName(name: String) {
|
||||
folderBackupDao.delete(name)
|
||||
}
|
||||
|
||||
/**************************************************************************************************************
|
||||
************************************************* Mappers ****************************************************
|
||||
**************************************************************************************************************/
|
||||
|
||||
private fun FolderBackUpConfiguration.toEntity(): FolderBackUpEntity =
|
||||
FolderBackUpEntity(
|
||||
accountName = accountName,
|
||||
behavior = behavior.toString(),
|
||||
sourcePath = sourcePath,
|
||||
uploadPath = uploadPath,
|
||||
wifiOnly = wifiOnly,
|
||||
chargingOnly = chargingOnly,
|
||||
name = name,
|
||||
lastSyncTimestamp = lastSyncTimestamp,
|
||||
spaceId = spaceId,
|
||||
)
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun FolderBackUpEntity.toModel() =
|
||||
FolderBackUpConfiguration(
|
||||
accountName = accountName,
|
||||
behavior = UploadBehavior.fromString(behavior),
|
||||
sourcePath = sourcePath,
|
||||
uploadPath = uploadPath,
|
||||
wifiOnly = wifiOnly,
|
||||
chargingOnly = chargingOnly,
|
||||
lastSyncTimestamp = lastSyncTimestamp,
|
||||
name = name,
|
||||
spaceId = spaceId,
|
||||
)
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.folderbackup.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
|
||||
@Entity(tableName = ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME)
|
||||
data class FolderBackUpEntity(
|
||||
val accountName: String,
|
||||
val behavior: String,
|
||||
val sourcePath: String,
|
||||
val uploadPath: String,
|
||||
val wifiOnly: Boolean,
|
||||
val chargingOnly: Boolean,
|
||||
val name: String,
|
||||
val lastSyncTimestamp: Long,
|
||||
val spaceId: String?,
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.folderbackup.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface FolderBackupDao {
|
||||
@Query(SELECT)
|
||||
fun getFolderBackUpConfigurationByName(
|
||||
name: String
|
||||
): FolderBackUpEntity?
|
||||
|
||||
@Query(SELECT)
|
||||
fun getFolderBackUpConfigurationByNameAsFlow(
|
||||
name: String
|
||||
): Flow<FolderBackUpEntity?>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(folderBackUpEntity: FolderBackUpEntity): Long
|
||||
|
||||
@Query(DELETE)
|
||||
fun delete(name: String): Int
|
||||
|
||||
@Transaction
|
||||
fun update(folderBackUpEntity: FolderBackUpEntity): Long {
|
||||
delete(folderBackUpEntity.name)
|
||||
return insertOrReplace(folderBackUpEntity)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SELECT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME}
|
||||
WHERE name = :name
|
||||
"""
|
||||
|
||||
private const val DELETE = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME}
|
||||
WHERE name = :name
|
||||
"""
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.folderbackup.repository
|
||||
|
||||
import eu.qsfera.android.data.folderbackup.datasources.LocalFolderBackupDataSource
|
||||
import eu.qsfera.android.domain.automaticuploads.FolderBackupRepository
|
||||
import eu.qsfera.android.domain.automaticuploads.model.AutomaticUploadsConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class OCFolderBackupRepository(
|
||||
private val localFolderBackupDataSource: LocalFolderBackupDataSource
|
||||
) : FolderBackupRepository {
|
||||
|
||||
override fun getAutomaticUploadsConfiguration(): AutomaticUploadsConfiguration? =
|
||||
localFolderBackupDataSource.getAutomaticUploadsConfiguration()
|
||||
|
||||
override fun getFolderBackupConfigurationByNameAsFlow(name: String): Flow<FolderBackUpConfiguration?> =
|
||||
localFolderBackupDataSource.getFolderBackupConfigurationByNameAsFlow(name)
|
||||
|
||||
override fun saveFolderBackupConfiguration(folderBackUpConfiguration: FolderBackUpConfiguration) {
|
||||
localFolderBackupDataSource.saveFolderBackupConfiguration(folderBackUpConfiguration)
|
||||
}
|
||||
|
||||
override fun resetFolderBackupConfigurationByName(name: String) =
|
||||
localFolderBackupDataSource.resetFolderBackupConfigurationByName(name)
|
||||
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* qsfera 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.DeleteColumn
|
||||
import androidx.room.RenameColumn
|
||||
import androidx.room.migration.AutoMigrationSpec
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_VERSION_MAJOR
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FILES_TABLE_NAME
|
||||
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "version_mayor",
|
||||
toColumnName = CAPABILITIES_VERSION_MAJOR
|
||||
)
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "enabled",
|
||||
toColumnName = "enabledAppProviders"
|
||||
)
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "version",
|
||||
toColumnName = "versionAppProviders"
|
||||
)
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "appsUrl",
|
||||
toColumnName = "appsUrlAppProviders"
|
||||
)
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "openUrl",
|
||||
toColumnName = "openUrlAppProviders"
|
||||
)
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "openWebUrl",
|
||||
toColumnName = "openWebUrlAppProviders"
|
||||
)
|
||||
@RenameColumn(
|
||||
tableName = CAPABILITIES_TABLE_NAME,
|
||||
fromColumnName = "newUrl",
|
||||
toColumnName = "newUrlAppProviders"
|
||||
)
|
||||
@DeleteColumn(
|
||||
tableName = FILES_TABLE_NAME,
|
||||
columnName = "lastSyncDateForProperties"
|
||||
)
|
||||
class AutoMigration39To40 : AutoMigrationSpec
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
val MIGRATION_27_28 = object : Migration(27, 28) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
" `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL," +
|
||||
" `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `search_min_length` INTEGER, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `sharing_public_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER" +
|
||||
" NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL" +
|
||||
" DEFAULT -1, `sharing_user_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning`" +
|
||||
" INTEGER NOT NULL DEFAULT -1)"
|
||||
)
|
||||
execSQL("ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} ADD COLUMN search_min_length INTEGER")
|
||||
execSQL(
|
||||
"INSERT INTO `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` SELECT id, account, version_mayor, version_minor, version_micro," +
|
||||
" version_string, version_edition,core_pollinterval, IFNULL(sharing_api_enabled, -1), search_min_length," +
|
||||
" IFNULL(sharing_public_enabled, -1), IFNULL(sharing_public_password_enforced, -1)," +
|
||||
"IFNULL(sharing_public_password_enforced_read_only, -1),IFNULL(sharing_public_password_enforced_read_write, -1)," +
|
||||
"IFNULL(sharing_public_password_enforced_public_only, -1),IFNULL(sharing_public_expire_date_enabled, -1)," +
|
||||
"IFNULL(sharing_public_expire_date_days, 0),IFNULL(sharing_public_expire_date_enforced, -1)," +
|
||||
"IFNULL(sharing_public_send_mail, -1),IFNULL(sharing_public_upload, -1),IFNULL(sharing_public_multiple, -1)," +
|
||||
" IFNULL(supports_upload_only, -1), IFNULL(sharing_user_send_mail, -1),IFNULL(sharing_resharing, -1)," +
|
||||
"IFNULL(sharing_federation_outgoing, -1),IFNULL(sharing_federation_incoming, -1),IFNULL(files_bigfilechunking, -1)," +
|
||||
"IFNULL(files_undelete, -1),IFNULL(files_versioning, -1) FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}"
|
||||
)
|
||||
execSQL("DROP TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
|
||||
execSQL("ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2 RENAME TO ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
val MIGRATION_28_29 = object : Migration(28, 29) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL("DROP TABLE ${ProviderTableMeta.OCSHARES_TABLE_NAME}")
|
||||
execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `${ProviderTableMeta.OCSHARES_TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
" `file_source` TEXT NOT NULL, `item_source` TEXT NOT NULL, `share_type` INTEGER NOT NULL, `shate_with` TEXT," +
|
||||
" `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date` INTEGER NOT NULL, `expiration_date` INTEGER" +
|
||||
" NOT NULL, `token` TEXT, `shared_with_display_name` TEXT, `share_with_additional_info` TEXT, `is_directory` INTEGER" +
|
||||
" NOT NULL, `user_id` INTEGER NOT NULL, `id_remote_shared` INTEGER NOT NULL, `owner_share` TEXT NOT NULL," +
|
||||
" `name` TEXT, `url` TEXT)"
|
||||
)
|
||||
execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
" `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER NOT NULL," +
|
||||
" `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `sharing_api_enabled` INTEGER NOT" +
|
||||
" NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT" +
|
||||
" NULL DEFAULT -1, `sharing_public_password_enforced_read_only` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `sharing_public_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT" +
|
||||
" NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT" +
|
||||
" -1, `sharing_user_send_mail` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning`" +
|
||||
" INTEGER NOT NULL DEFAULT -1)"
|
||||
)
|
||||
execSQL(
|
||||
"INSERT INTO `${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` SELECT id, account, version_mayor, version_minor, version_micro," +
|
||||
" version_string, version_edition, core_pollinterval, IFNULL(sharing_api_enabled, -1), IFNULL(sharing_public_enabled, -1)," +
|
||||
" IFNULL(sharing_public_password_enforced, -1),IFNULL(sharing_public_password_enforced_read_only, -1)," +
|
||||
"IFNULL(sharing_public_password_enforced_read_write, -1),IFNULL(sharing_public_password_enforced_public_only, -1)," +
|
||||
"IFNULL(sharing_public_expire_date_enabled, -1),IFNULL(sharing_public_expire_date_days, 0)," +
|
||||
"IFNULL(sharing_public_expire_date_enforced, -1),IFNULL(sharing_public_send_mail, -1),IFNULL(sharing_public_upload, -1)," +
|
||||
"IFNULL(sharing_public_multiple, -1), IFNULL(supports_upload_only, -1), IFNULL(sharing_user_send_mail, -1)," +
|
||||
"IFNULL(sharing_resharing, -1),IFNULL(sharing_federation_outgoing, -1),IFNULL(sharing_federation_incoming, -1)," +
|
||||
"IFNULL(files_bigfilechunking, -1),IFNULL(files_undelete, -1),IFNULL(files_versioning, -1) " +
|
||||
"FROM ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}"
|
||||
)
|
||||
execSQL("DROP TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
|
||||
execSQL("ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}2 RENAME TO ${ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
|
||||
val MIGRATION_29_30 = object : Migration(29, 30) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT" +
|
||||
" NOT NULL, `account` TEXT, `version_mayor` INTEGER NOT NULL, `version_minor` INTEGER NOT NULL, `version_micro` INTEGER" +
|
||||
" NOT NULL, `version_string` TEXT, `version_edition` TEXT, `core_pollinterval` INTEGER NOT NULL, `dav_chunking_version`" +
|
||||
" TEXT NOT NULL DEFAULT '',`sharing_api_enabled` INTEGER NOT NULL DEFAULT -1, `sharing_public_enabled` INTEGER NOT NULL" +
|
||||
" DEFAULT -1, `sharing_public_password_enforced` INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_only`" +
|
||||
" INTEGER NOT NULL DEFAULT -1, `sharing_public_password_enforced_read_write` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_public_password_enforced_public_only` INTEGER NOT NULL DEFAULT -1, `sharing_public_expire_date_enabled` INTEGER" +
|
||||
" NOT NULL DEFAULT -1, `sharing_public_expire_date_days` INTEGER NOT NULL, `sharing_public_expire_date_enforced` INTEGER" +
|
||||
" NOT NULL DEFAULT -1, `sharing_public_upload` INTEGER NOT NULL DEFAULT -1, `sharing_public_multiple` INTEGER NOT NULL" +
|
||||
" DEFAULT -1, `supports_upload_only` INTEGER NOT NULL DEFAULT -1, `sharing_resharing` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `sharing_federation_outgoing` INTEGER NOT NULL DEFAULT -1, `sharing_federation_incoming` INTEGER NOT NULL DEFAULT -1," +
|
||||
" `files_bigfilechunking` INTEGER NOT NULL DEFAULT -1, `files_undelete` INTEGER NOT NULL DEFAULT -1, `files_versioning`" +
|
||||
" INTEGER NOT NULL DEFAULT -1)"
|
||||
)
|
||||
execSQL(
|
||||
"INSERT INTO `${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}2` SELECT id, account, version_mayor, version_minor," +
|
||||
" version_micro, version_string, version_edition, core_pollinterval, '', IFNULL(sharing_api_enabled, -1)," +
|
||||
" IFNULL(sharing_public_enabled, -1), IFNULL(sharing_public_password_enforced, -1)," +
|
||||
"IFNULL(sharing_public_password_enforced_read_only, -1),IFNULL(sharing_public_password_enforced_read_write, -1)," +
|
||||
"IFNULL(sharing_public_password_enforced_public_only, -1),IFNULL(sharing_public_expire_date_enabled, -1)," +
|
||||
"IFNULL(sharing_public_expire_date_days, 0),IFNULL(sharing_public_expire_date_enforced, -1)," +
|
||||
" IFNULL(sharing_public_upload, -1),IFNULL(sharing_public_multiple, -1), IFNULL(supports_upload_only, -1)," +
|
||||
" IFNULL(sharing_resharing, -1),IFNULL(sharing_federation_outgoing, -1),IFNULL(sharing_federation_incoming, -1)," +
|
||||
"IFNULL(files_bigfilechunking, -1),IFNULL(files_undelete, -1),IFNULL(files_versioning, -1) " +
|
||||
"FROM ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}"
|
||||
)
|
||||
execSQL("DROP TABLE ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
|
||||
execSQL("ALTER TABLE ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}2 RENAME TO" +
|
||||
" ${ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME}")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val MIGRATION_30_31 = object : Migration(30, 31) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
//Nothing to migrate at the moment
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val MIGRATION_31_32 = object : Migration(31, 32) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `user_quotas` (`accountName` TEXT NOT NULL, `used` INTEGER NOT NULL," +
|
||||
" `available` INTEGER NOT NULL, PRIMARY KEY(`accountName`))")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.database.sqlite.SQLiteException
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.OCSHARES_TABLE_NAME
|
||||
import timber.log.Timber
|
||||
|
||||
val MIGRATION_32_33 = object : Migration(32, 33) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
|
||||
try {
|
||||
// 1. Create new OCShares table
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `${OCSHARES_TABLE_NAME}2` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
|
||||
" `share_type` INTEGER NOT NULL, `share_with` TEXT, `path` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `shared_date`" +
|
||||
" INTEGER NOT NULL, `expiration_date` INTEGER NOT NULL, `token` TEXT, `shared_with_display_name` TEXT," +
|
||||
" `share_with_additional_info` TEXT, `is_directory` INTEGER NOT NULL, `id_remote_shared` TEXT NOT NULL, `owner_share`" +
|
||||
" TEXT NOT NULL, `name` TEXT, `url` TEXT)")
|
||||
|
||||
// 2. Get old OCShares and insert them into the new table
|
||||
val cursor = database.query("SELECT * FROM $OCSHARES_TABLE_NAME")
|
||||
cursor.use {
|
||||
while (it.moveToNext()) {
|
||||
val cv = ContentValues()
|
||||
cv.put("id", it.getInt(it.getColumnIndexOrThrow("id")))
|
||||
cv.put("share_type", it.getInt(it.getColumnIndexOrThrow("share_type")))
|
||||
cv.put("share_with", it.getString(it.getColumnIndexOrThrow("shate_with")))
|
||||
cv.put("path", it.getString(it.getColumnIndexOrThrow("path")))
|
||||
cv.put("permissions", it.getInt(it.getColumnIndexOrThrow("permissions")))
|
||||
cv.put("shared_date", it.getInt(it.getColumnIndexOrThrow("shared_date")))
|
||||
cv.put("expiration_date", it.getInt(it.getColumnIndexOrThrow("expiration_date")))
|
||||
cv.put("token", it.getString(it.getColumnIndexOrThrow("token")))
|
||||
cv.put("shared_with_display_name", it.getString(it.getColumnIndexOrThrow("shared_with_display_name")))
|
||||
cv.put("share_with_additional_info", it.getString(it.getColumnIndexOrThrow("share_with_additional_info")))
|
||||
cv.put("is_directory", it.getInt(it.getColumnIndexOrThrow("is_directory")))
|
||||
cv.put("id_remote_shared", it.getString(it.getColumnIndexOrThrow("id_remote_shared")))
|
||||
cv.put("owner_share", it.getString(it.getColumnIndexOrThrow("owner_share")))
|
||||
cv.put("name", it.getString(it.getColumnIndexOrThrow("name")))
|
||||
cv.put("url", it.getString(it.getColumnIndexOrThrow("url")))
|
||||
|
||||
database.insert("${OCSHARES_TABLE_NAME}2", 0, cv)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Drop old table and rename new one.
|
||||
database.execSQL("DROP TABLE $OCSHARES_TABLE_NAME")
|
||||
database.execSQL("ALTER TABLE ${OCSHARES_TABLE_NAME}2 RENAME TO $OCSHARES_TABLE_NAME")
|
||||
|
||||
} catch (e: SQLiteException) {
|
||||
Timber.e(e, "SQLiteException in migrate from database version 32 to version 33")
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to migrate database version 32 to version 33")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME
|
||||
import eu.qsfera.android.data.providers.SharedPreferencesProvider
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.pictureUploadsName
|
||||
import eu.qsfera.android.domain.automaticuploads.model.FolderBackUpConfiguration.Companion.videoUploadsName
|
||||
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
|
||||
import java.io.File
|
||||
|
||||
val MIGRATION_33_34 = object : Migration(33, 34) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `$FOLDER_BACKUP_TABLE_NAME` (`accountName` TEXT NOT NULL, `behavior` TEXT NOT NULL," +
|
||||
" `sourcePath` TEXT NOT NULL, `uploadPath` TEXT NOT NULL, `wifiOnly` INTEGER NOT NULL, `name` TEXT NOT NULL," +
|
||||
" `lastSyncTimestamp` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)")
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Legacy code. Only used to migrate old camera uploads configuration from ")
|
||||
class CameraUploadsMigrationToRoom(val sharedPreferencesProvider: SharedPreferencesProvider) {
|
||||
|
||||
fun getPictureUploadsConfigurationPreferences(timestamp: Long): FolderBackUpConfiguration? {
|
||||
|
||||
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_ENABLED, false)) return null
|
||||
|
||||
return FolderBackUpConfiguration(
|
||||
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME, null) ?: "",
|
||||
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY, false),
|
||||
uploadPath = getUploadPathForPreference(PREF__CAMERA_PICTURE_UPLOADS_PATH),
|
||||
sourcePath = getSourcePathForPreference(PREF__CAMERA_PICTURE_UPLOADS_SOURCE),
|
||||
behavior = getBehaviorForPreference(PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR),
|
||||
lastSyncTimestamp = timestamp,
|
||||
name = pictureUploadsName,
|
||||
chargingOnly = false,
|
||||
spaceId = null,
|
||||
)
|
||||
}
|
||||
|
||||
fun getVideoUploadsConfigurationPreferences(timestamp: Long): FolderBackUpConfiguration? {
|
||||
if (!sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_ENABLED, false)) return null
|
||||
|
||||
return FolderBackUpConfiguration(
|
||||
accountName = sharedPreferencesProvider.getString(PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME, null) ?: "",
|
||||
wifiOnly = sharedPreferencesProvider.getBoolean(PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY, false),
|
||||
uploadPath = getUploadPathForPreference(PREF__CAMERA_VIDEO_UPLOADS_PATH),
|
||||
sourcePath = getSourcePathForPreference(PREF__CAMERA_VIDEO_UPLOADS_SOURCE),
|
||||
behavior = getBehaviorForPreference(PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR),
|
||||
lastSyncTimestamp = timestamp,
|
||||
name = videoUploadsName,
|
||||
chargingOnly = false,
|
||||
spaceId = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUploadPathForPreference(keyPreference: String): String {
|
||||
val uploadPath = sharedPreferencesProvider.getString(
|
||||
key = keyPreference,
|
||||
defaultValue = DEFAULT_PATH_FOR_CAMERA_UPLOADS + File.separator
|
||||
)
|
||||
return if (uploadPath!!.endsWith(File.separator)) uploadPath else uploadPath + File.separator
|
||||
}
|
||||
|
||||
private fun getSourcePathForPreference(keyPreference: String): String =
|
||||
sharedPreferencesProvider.getString(keyPreference, null) ?: ""
|
||||
|
||||
private fun getBehaviorForPreference(keyPreference: String): UploadBehavior {
|
||||
val storedBehaviour = sharedPreferencesProvider.getString(keyPreference, null) ?: return UploadBehavior.COPY
|
||||
|
||||
return UploadBehavior.fromString(storedBehaviour)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREF__CAMERA_PICTURE_UPLOADS_ENABLED = "enable_picture_uploads"
|
||||
private const val PREF__CAMERA_VIDEO_UPLOADS_ENABLED = "enable_video_uploads"
|
||||
private const val PREF__CAMERA_PICTURE_UPLOADS_WIFI_ONLY = "picture_uploads_on_wifi"
|
||||
private const val PREF__CAMERA_VIDEO_UPLOADS_WIFI_ONLY = "video_uploads_on_wifi"
|
||||
private const val PREF__CAMERA_PICTURE_UPLOADS_PATH = "picture_uploads_path"
|
||||
private const val PREF__CAMERA_VIDEO_UPLOADS_PATH = "video_uploads_path"
|
||||
private const val PREF__CAMERA_PICTURE_UPLOADS_BEHAVIOUR = "picture_uploads_behaviour"
|
||||
private const val PREF__CAMERA_PICTURE_UPLOADS_SOURCE = "picture_uploads_source_path"
|
||||
private const val PREF__CAMERA_VIDEO_UPLOADS_BEHAVIOUR = "video_uploads_behaviour"
|
||||
private const val PREF__CAMERA_VIDEO_UPLOADS_SOURCE = "video_uploads_source_path"
|
||||
private const val PREF__CAMERA_PICTURE_UPLOADS_ACCOUNT_NAME = "picture_uploads_account_name"
|
||||
private const val PREF__CAMERA_VIDEO_UPLOADS_ACCOUNT_NAME = "video_uploads_account_name"
|
||||
|
||||
private const val DEFAULT_PATH_FOR_CAMERA_UPLOADS = "/CameraUpload"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Fernando Sanz Velasco
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME
|
||||
|
||||
val MIGRATION_34_35 = object : Migration(34, 35) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL("ALTER TABLE $FOLDER_BACKUP_TABLE_NAME ADD COLUMN chargingOnly INTEGER NOT NULL DEFAULT '0'")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Fernando Sanz Velasco
|
||||
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.CAPABILITIES_TABLE_NAME
|
||||
|
||||
val MIGRATION_35_36 = object : Migration(35, 36) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL("ALTER TABLE $CAPABILITIES_TABLE_NAME ADD COLUMN sharing_user_profile_picture INTEGER NOT NULL DEFAULT -1")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* qsfera 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val MIGRATION_37_38 = object : Migration(37, 38) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `files` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `parentId` INTEGER," +
|
||||
" `owner` TEXT NOT NULL, `remotePath` TEXT NOT NULL, `remoteId` TEXT, `length` INTEGER NOT NULL, `creationTimestamp` INTEGER," +
|
||||
" `modificationTimestamp` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `etag` TEXT, `permissions` TEXT, `privateLink` TEXT," +
|
||||
" `storagePath` TEXT, `name` TEXT, `treeEtag` TEXT, `keepInSync` INTEGER, `lastSyncDateForData` INTEGER, `fileShareViaLink`" +
|
||||
" INTEGER, `lastSyncDateForProperties` INTEGER, `needsToUpdateThumbnail` INTEGER NOT NULL, `modifiedAtLastSyncForData` INTEGER," +
|
||||
" `etagInConflict` TEXT, `fileIsDownloading` INTEGER, `sharedWithSharee` INTEGER, `sharedByLink` INTEGER NOT NULL)")
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `transfers` (`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)")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Aitor Ballesteros Pavó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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val MIGRATION_41_42 = object : Migration(41, 42) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL("ALTER TABLE `files` ADD COLUMN `lastUsage` INTEGER")
|
||||
|
||||
execSQL("UPDATE `files` SET `lastUsage` = CASE WHEN `storagePath` IS NOT NULL THEN ${System.currentTimeMillis()} ELSE NULL END")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Aitor Ballesteros Pavó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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import timber.log.Timber
|
||||
|
||||
val MIGRATION_42_43 = object : Migration(42, 43) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.run {
|
||||
execSQL("ALTER TABLE ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME} ADD COLUMN `spaceId` TEXT")
|
||||
val query = "SELECT `accountName` FROM ${ProviderMeta.ProviderTableMeta.FOLDER_BACKUP_TABLE_NAME}"
|
||||
val cursor = database.query(query)
|
||||
cursor.use {
|
||||
while (it.moveToNext()) {
|
||||
val accountName = it.getString(it.getColumnIndexOrThrow("accountName"))
|
||||
|
||||
val spacePersonalQuery = "SELECT `space_id` FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}\n" +
|
||||
"WHERE `account_name` = '$accountName' AND `drive_type`= 'personal'"
|
||||
val cursorSpacePersonal = database.query(spacePersonalQuery)
|
||||
|
||||
cursorSpacePersonal.use {
|
||||
if (cursorSpacePersonal.moveToFirst()) {
|
||||
val spaceId = cursorSpacePersonal.getString(cursorSpacePersonal.getColumnIndexOrThrow("space_id"))
|
||||
execSQL("UPDATE `folder_backup` SET `spaceId` = '$spaceId' WHERE `accountName` = '$accountName'")
|
||||
} else {
|
||||
execSQL("UPDATE `folder_backup` SET `spaceId` = NULL WHERE `accountName` = '$accountName'")
|
||||
Timber.d("No personal spaces found for account: $accountName.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
val MIGRATION_47_48 = object : Migration(47, 48) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
|
||||
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_VERSION}` TEXT"
|
||||
)
|
||||
database.execSQL(
|
||||
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
|
||||
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_RESUMABLE}` TEXT"
|
||||
)
|
||||
database.execSQL(
|
||||
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
|
||||
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_EXTENSION}` TEXT"
|
||||
)
|
||||
database.execSQL(
|
||||
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
|
||||
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_MAX_CHUNK_SIZE}` INTEGER"
|
||||
)
|
||||
database.execSQL(
|
||||
"ALTER TABLE ${ProviderTableMeta.CAPABILITIES_TABLE_NAME} " +
|
||||
"ADD COLUMN `${ProviderTableMeta.CAPABILITIES_TUS_SUPPORT_HTTP_METHOD_OVERRIDE}` TEXT"
|
||||
)
|
||||
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadUrl` TEXT")
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadLength` INTEGER")
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadMetadata` TEXT")
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadChecksum` TEXT")
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusResumableVersion` TEXT")
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadExpires` INTEGER")
|
||||
database.execSQL("ALTER TABLE ${ProviderTableMeta.TRANSFERS_TABLE_NAME} ADD COLUMN `tusUploadConcat` TEXT")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package eu.qsfera.android.data.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
val MIGRATION_48_49 = object : Migration(48, 49) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"ALTER TABLE ${ProviderTableMeta.FILES_TABLE_NAME} " +
|
||||
"ADD COLUMN `${ProviderTableMeta.FILE_REMOTE_ETAG}` TEXT DEFAULT NULL"
|
||||
)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.oauth.datasources
|
||||
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationRequest
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.OIDCServerConfiguration
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.TokenRequest
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.TokenResponse
|
||||
|
||||
interface RemoteOAuthDataSource {
|
||||
fun performOIDCDiscovery(baseUrl: String): OIDCServerConfiguration
|
||||
fun performTokenRequest(tokenRequest: TokenRequest): TokenResponse
|
||||
|
||||
fun registerClient(clientRegistrationRequest: ClientRegistrationRequest): ClientRegistrationInfo
|
||||
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.oauth.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.oauth.datasources.RemoteOAuthDataSource
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationRequest
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.OIDCServerConfiguration
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.TokenRequest
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.TokenResponse
|
||||
import eu.qsfera.android.lib.resources.oauth.params.ClientRegistrationParams
|
||||
import eu.qsfera.android.lib.resources.oauth.params.TokenRequestParams
|
||||
import eu.qsfera.android.lib.resources.oauth.responses.ClientRegistrationResponse
|
||||
import eu.qsfera.android.lib.resources.oauth.responses.OIDCDiscoveryResponse
|
||||
import eu.qsfera.android.lib.resources.oauth.services.OIDCService
|
||||
import eu.qsfera.android.lib.resources.oauth.responses.TokenResponse as RemoteTokenResponse
|
||||
|
||||
class OCRemoteOAuthDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
private val oidcService: OIDCService,
|
||||
) : RemoteOAuthDataSource {
|
||||
|
||||
override fun performOIDCDiscovery(baseUrl: String): OIDCServerConfiguration {
|
||||
val qsferaClient = clientManager.getClientForAnonymousCredentials(baseUrl, false)
|
||||
|
||||
val serverConfiguration = executeRemoteOperation {
|
||||
oidcService.getOIDCServerDiscovery(qsferaClient)
|
||||
}
|
||||
|
||||
return serverConfiguration.toModel()
|
||||
}
|
||||
|
||||
override fun performTokenRequest(tokenRequest: TokenRequest): TokenResponse {
|
||||
// For token refreshments, a new client is required, otherwise it could keep outdated credentials or data.
|
||||
val requiresNewClient = tokenRequest is TokenRequest.RefreshToken
|
||||
val qsferaClient = clientManager.getClientForAnonymousCredentials(path = tokenRequest.baseUrl, requiresNewClient = requiresNewClient)
|
||||
|
||||
val tokenResponse = executeRemoteOperation {
|
||||
oidcService.performTokenRequest(
|
||||
qsferaClient = qsferaClient,
|
||||
tokenRequest = tokenRequest.toParams()
|
||||
)
|
||||
}
|
||||
|
||||
return tokenResponse.toModel()
|
||||
}
|
||||
|
||||
override fun registerClient(clientRegistrationRequest: ClientRegistrationRequest): ClientRegistrationInfo {
|
||||
val qsferaClient =
|
||||
clientManager.getClientForAnonymousCredentials(clientRegistrationRequest.registrationEndpoint, false)
|
||||
|
||||
val remoteClientRegistrationInfo = executeRemoteOperation {
|
||||
oidcService.registerClientWithRegistrationEndpoint(
|
||||
qsferaClient = qsferaClient,
|
||||
clientRegistrationParams = clientRegistrationRequest.toParams()
|
||||
)
|
||||
}
|
||||
|
||||
return remoteClientRegistrationInfo.toModel()
|
||||
}
|
||||
|
||||
/**************************************************************************************************************
|
||||
************************************************* Mappers ****************************************************
|
||||
**************************************************************************************************************/
|
||||
private fun OIDCDiscoveryResponse.toModel(): OIDCServerConfiguration =
|
||||
OIDCServerConfiguration(
|
||||
authorizationEndpoint = this.authorizationEndpoint,
|
||||
checkSessionIframe = this.checkSessionIframe,
|
||||
endSessionEndpoint = this.endSessionEndpoint,
|
||||
issuer = this.issuer,
|
||||
registrationEndpoint = this.registrationEndpoint,
|
||||
responseTypesSupported = this.responseTypesSupported,
|
||||
scopesSupported = this.scopesSupported,
|
||||
tokenEndpoint = this.tokenEndpoint,
|
||||
tokenEndpointAuthMethodsSupported = this.tokenEndpointAuthMethodsSupported,
|
||||
userInfoEndpoint = this.userinfoEndpoint
|
||||
)
|
||||
|
||||
private fun TokenRequest.toParams(): TokenRequestParams =
|
||||
when (this) {
|
||||
is TokenRequest.AccessToken ->
|
||||
TokenRequestParams.Authorization(
|
||||
tokenEndpoint = this.tokenEndpoint,
|
||||
authorizationCode = this.authorizationCode,
|
||||
grantType = this.grantType,
|
||||
scope = this.scope,
|
||||
clientId = this.clientId,
|
||||
clientSecret = this.clientSecret,
|
||||
redirectUri = this.redirectUri,
|
||||
clientAuth = this.clientAuth,
|
||||
codeVerifier = this.codeVerifier
|
||||
)
|
||||
is TokenRequest.RefreshToken ->
|
||||
TokenRequestParams.RefreshToken(
|
||||
tokenEndpoint = this.tokenEndpoint,
|
||||
grantType = this.grantType,
|
||||
scope = this.scope,
|
||||
clientId = this.clientId,
|
||||
clientSecret = this.clientSecret,
|
||||
clientAuth = this.clientAuth,
|
||||
refreshToken = this.refreshToken
|
||||
)
|
||||
}
|
||||
|
||||
private fun RemoteTokenResponse.toModel(): TokenResponse =
|
||||
TokenResponse(
|
||||
accessToken = this.accessToken,
|
||||
expiresIn = this.expiresIn,
|
||||
refreshToken = this.refreshToken,
|
||||
tokenType = this.tokenType,
|
||||
userId = this.userId,
|
||||
scope = this.scope,
|
||||
idToken = this.idToken,
|
||||
additionalParameters = this.additionalParameters
|
||||
)
|
||||
|
||||
private fun ClientRegistrationRequest.toParams(): ClientRegistrationParams =
|
||||
ClientRegistrationParams(
|
||||
registrationEndpoint = this.registrationEndpoint,
|
||||
clientName = this.clientName,
|
||||
redirectUris = this.redirectUris,
|
||||
tokenEndpointAuthMethod = this.tokenEndpointAuthMethod,
|
||||
applicationType = this.applicationType
|
||||
)
|
||||
|
||||
private fun ClientRegistrationResponse.toModel(): ClientRegistrationInfo =
|
||||
ClientRegistrationInfo(
|
||||
clientId = this.clientId,
|
||||
clientSecret = this.clientSecret,
|
||||
clientIdIssuedAt = this.clientIdIssuedAt,
|
||||
clientSecretExpiration = this.clientSecretExpiration
|
||||
)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.oauth.repository
|
||||
|
||||
import eu.qsfera.android.data.oauth.datasources.RemoteOAuthDataSource
|
||||
import eu.qsfera.android.domain.authentication.oauth.OAuthRepository
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationInfo
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.ClientRegistrationRequest
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.OIDCServerConfiguration
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.TokenRequest
|
||||
import eu.qsfera.android.domain.authentication.oauth.model.TokenResponse
|
||||
|
||||
class OCOAuthRepository(
|
||||
private val oidcRemoteOAuthDataSource: RemoteOAuthDataSource,
|
||||
) : OAuthRepository {
|
||||
|
||||
override fun performOIDCDiscovery(baseUrl: String): OIDCServerConfiguration =
|
||||
oidcRemoteOAuthDataSource.performOIDCDiscovery(baseUrl)
|
||||
|
||||
override fun performTokenRequest(tokenRequest: TokenRequest): TokenResponse =
|
||||
oidcRemoteOAuthDataSource.performTokenRequest(tokenRequest)
|
||||
|
||||
override fun registerClient(clientRegistrationRequest: ClientRegistrationRequest): ClientRegistrationInfo =
|
||||
oidcRemoteOAuthDataSource.registerClient(clientRegistrationRequest)
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
*
|
||||
* Copyright (C) 2021 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.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.providers
|
||||
|
||||
import android.os.Environment
|
||||
import java.io.File
|
||||
|
||||
@Deprecated("Do not use this anymore. We have moved to Scoped Storage")
|
||||
class LegacyStorageProvider(
|
||||
rootFolderName: String
|
||||
) : LocalStorageProvider(rootFolderName) {
|
||||
|
||||
override fun getPrimaryStorageDirectory(): File = Environment.getExternalStorageDirectory()
|
||||
}
|
||||
+291
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David A. Velasco
|
||||
* @author David González Verdugo
|
||||
* @author Christian Schabesberger
|
||||
* @author Shashvat Kedia
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.providers
|
||||
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import eu.qsfera.android.data.extensions.moveRecursively
|
||||
import eu.qsfera.android.domain.files.model.OCFile
|
||||
import eu.qsfera.android.domain.transfers.model.OCTransfer
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
sealed class LocalStorageProvider(private val rootFolderName: String) {
|
||||
|
||||
abstract fun getPrimaryStorageDirectory(): File
|
||||
|
||||
/**
|
||||
* Return the root path of primary shared/external storage directory for this application.
|
||||
* For example: /storage/emulated/0/qsfera
|
||||
*/
|
||||
fun getRootFolderPath(): String = getPrimaryStorageDirectory().absolutePath + File.separator + rootFolderName
|
||||
|
||||
/**
|
||||
* Get local storage path for accountName.
|
||||
*/
|
||||
private fun getAccountDirectoryPath(
|
||||
accountName: String
|
||||
): String = getRootFolderPath() + File.separator + getEncodedAccountName(accountName)
|
||||
|
||||
/**
|
||||
* Get local path where OCFile file is to be stored after upload. That is,
|
||||
* corresponding local path (in local qsfera storage) to remote uploaded
|
||||
* file.
|
||||
*/
|
||||
fun getDefaultSavePathFor(
|
||||
accountName: String,
|
||||
remotePath: String,
|
||||
spaceId: String?,
|
||||
): String =
|
||||
if (spaceId != null) {
|
||||
getAccountDirectoryPath(accountName) + File.separator + spaceId + File.separator + remotePath
|
||||
} else {
|
||||
getAccountDirectoryPath(accountName) + remotePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expected remote path for a file creation, rename, move etc
|
||||
*/
|
||||
fun getExpectedRemotePath(remotePath: String, newName: String, isFolder: Boolean): String {
|
||||
var parent = (File(remotePath)).parent ?: throw IllegalArgumentException("Parent path is null")
|
||||
parent = if (parent.endsWith(File.separator)) parent else parent + File.separator
|
||||
var newRemotePath = parent + newName
|
||||
if (isFolder) {
|
||||
newRemotePath += File.separator
|
||||
}
|
||||
return newRemotePath
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path to tmp folder inside datafolder in sd-card for given accountName.
|
||||
*/
|
||||
fun getTemporalPath(
|
||||
accountName: String?,
|
||||
spaceId: String? = null,
|
||||
): String {
|
||||
val temporalPathWithoutSpace =
|
||||
getRootFolderPath() + File.separator + TEMPORAL_FOLDER_NAME + File.separator + getEncodedAccountName(accountName)
|
||||
|
||||
return if (spaceId != null) {
|
||||
temporalPathWithoutSpace + File.separator + spaceId
|
||||
} else {
|
||||
temporalPathWithoutSpace
|
||||
}
|
||||
}
|
||||
|
||||
fun getLogsPath(): String = getRootFolderPath() + File.separator + LOGS_FOLDER_NAME + File.separator
|
||||
|
||||
/**
|
||||
* Optimistic number of bytes available on sd-card.
|
||||
*
|
||||
* @return Optimistic number of available bytes (can be less)
|
||||
*/
|
||||
@SuppressLint("UsableSpace")
|
||||
fun getUsableSpace(): Long = getPrimaryStorageDirectory().usableSpace
|
||||
|
||||
/**
|
||||
* Checks if there is user data which does not have a corresponding account in the Account manager.
|
||||
*/
|
||||
private fun getDanglingAccountDirs(remainingAccounts: Array<Account>): List<File> {
|
||||
val rootFolder = File(getRootFolderPath())
|
||||
val danglingDirs = mutableListOf<File>()
|
||||
rootFolder.listFiles()?.forEach { dir ->
|
||||
var dirIsOk = false
|
||||
if (dir.name.equals(TEMPORAL_FOLDER_NAME) || dir.name.equals(LOGS_FOLDER_NAME)) {
|
||||
dirIsOk = true
|
||||
} else {
|
||||
remainingAccounts.forEach { account ->
|
||||
if (dir.name.equals(getEncodedAccountName(account.name))) {
|
||||
dirIsOk = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!dirIsOk) {
|
||||
danglingDirs.add(dir)
|
||||
}
|
||||
}
|
||||
return danglingDirs
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up unused files, such as deprecated user directories
|
||||
*/
|
||||
open fun deleteUnusedUserDirs(remainingAccounts: Array<Account>) {
|
||||
val danglingDirs = getDanglingAccountDirs(remainingAccounts)
|
||||
danglingDirs.forEach { dd ->
|
||||
dd.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL encoding is an 'easy fix' to overcome that NTFS and FAT32 don't allow ":" in file names,
|
||||
* that can be in the accountName since 0.1.190B
|
||||
*/
|
||||
private fun getEncodedAccountName(accountName: String?): String = Uri.encode(accountName, "@")
|
||||
|
||||
fun moveLegacyToScopedStorage() {
|
||||
val timeInMillis = measureTimeMillis {
|
||||
moveFileOrFolderToScopedStorage(retrieveRootLegacyStorage())
|
||||
}
|
||||
Timber.d("MIGRATED FILES IN ${TimeUnit.SECONDS.convert(timeInMillis, TimeUnit.MILLISECONDS)} seconds")
|
||||
}
|
||||
|
||||
private fun retrieveRootLegacyStorage(): File {
|
||||
val legacyStorageProvider = LegacyStorageProvider(rootFolderName)
|
||||
val rootLegacyStorage = File(legacyStorageProvider.getRootFolderPath())
|
||||
|
||||
val legacyStorageUsedBytes = sizeOfDirectory(rootLegacyStorage)
|
||||
Timber.d(
|
||||
"Root ${rootLegacyStorage.absolutePath} has ${rootLegacyStorage.listFiles()?.size} files and its size is $legacyStorageUsedBytes Bytes"
|
||||
)
|
||||
|
||||
return rootLegacyStorage
|
||||
}
|
||||
|
||||
private fun moveFileOrFolderToScopedStorage(rootLegacyDirectory: File) {
|
||||
Timber.d("Let's move ${rootLegacyDirectory.absolutePath} to scoped storage")
|
||||
rootLegacyDirectory.listFiles()?.forEach { file ->
|
||||
if (file.isDirectory) {
|
||||
file.moveRecursively(File(getRootFolderPath(), file.name), overwrite = true)
|
||||
}
|
||||
}
|
||||
rootLegacyDirectory.deleteRecursively()
|
||||
}
|
||||
|
||||
fun sizeOfDirectory(dir: File): Long {
|
||||
if (dir.exists()) {
|
||||
var result: Long = 0
|
||||
val fileList = dir.listFiles() ?: arrayOf()
|
||||
fileList.forEach { file ->
|
||||
// Recursive call if it's a directory
|
||||
result += if (file.isDirectory) {
|
||||
sizeOfDirectory(file)
|
||||
} else {
|
||||
// Sum the file size in bytes
|
||||
file.length()
|
||||
}
|
||||
}
|
||||
return result // return the file size
|
||||
}
|
||||
return 0
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort to remove the file locally. If storage path is null, let's try to remove it anyway.
|
||||
*/
|
||||
fun deleteLocalFile(ocFile: OCFile): Boolean {
|
||||
val safeStoragePath = ocFile.getStoragePathOrExpectedPathForFile()
|
||||
val fileToDelete = File(safeStoragePath)
|
||||
|
||||
if (!fileToDelete.exists()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return fileToDelete.deleteRecursively()
|
||||
}
|
||||
|
||||
fun deleteLocalFolderIfItHasNoFilesInside(ocFolder: OCFile) {
|
||||
val safeStoragePath = ocFolder.getStoragePathOrExpectedPathForFile()
|
||||
val folder = File(safeStoragePath)
|
||||
|
||||
val filesInFolder = folder.listFiles()
|
||||
if (filesInFolder.isNullOrEmpty()) {
|
||||
folder.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun moveLocalFile(ocFile: OCFile, finalStoragePath: String) {
|
||||
val safeStoragePath = ocFile.getStoragePathOrExpectedPathForFile()
|
||||
val fileToMove = File(safeStoragePath)
|
||||
|
||||
if (!fileToMove.exists()) {
|
||||
return
|
||||
}
|
||||
val targetFile = File(finalStoragePath)
|
||||
val targetFolder = targetFile.parentFile
|
||||
if (targetFolder != null && !targetFolder.exists()) {
|
||||
targetFolder.mkdirs()
|
||||
}
|
||||
fileToMove.renameTo(targetFile)
|
||||
}
|
||||
|
||||
fun clearUnrelatedTemporalFiles(uploads: List<OCTransfer>, accountsNames: List<String>) {
|
||||
accountsNames.forEach { accountName ->
|
||||
val temporalFolderForAccount = File(getTemporalPath(accountName))
|
||||
cleanTemporalRecursively(temporalFolderForAccount) { temporalFile ->
|
||||
if (!uploads.map { it.localPath }.contains(temporalFile.absolutePath)) {
|
||||
temporalFile.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanTemporalRecursively(temporalFolder: File, deleteFileInCaseItIsNotNeeded: (file: File) -> Unit) {
|
||||
temporalFolder.listFiles()?.forEach { temporalFile ->
|
||||
if (temporalFile.isDirectory) {
|
||||
cleanTemporalRecursively(temporalFile, deleteFileInCaseItIsNotNeeded)
|
||||
} else {
|
||||
deleteFileInCaseItIsNotNeeded(temporalFile)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLocalStorageForAccount(accountName: String) {
|
||||
val mainFolderForAccount = File(getAccountDirectoryPath(accountName))
|
||||
val temporalFolderForAccount = File(getTemporalPath(accountName))
|
||||
mainFolderForAccount.deleteRecursively()
|
||||
temporalFolderForAccount.deleteRecursively()
|
||||
}
|
||||
|
||||
fun deleteCacheIfNeeded(transfer: OCTransfer) {
|
||||
val cacheDir = getTemporalPath(transfer.accountName)
|
||||
if (transfer.localPath.startsWith(cacheDir)) {
|
||||
val cacheFile = File(transfer.localPath)
|
||||
cacheFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the storage path if the file is already in the device storage or
|
||||
* the expected storage path for the file in case it's not available locally yet.
|
||||
*/
|
||||
private fun OCFile.getStoragePathOrExpectedPathForFile() =
|
||||
storagePath.takeUnless { it.isNullOrBlank() } ?: getDefaultSavePathFor(
|
||||
accountName = owner,
|
||||
remotePath = remotePath,
|
||||
spaceId = spaceId,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val LOGS_FOLDER_NAME = "logs"
|
||||
private const val TEMPORAL_FOLDER_NAME = "tmp"
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
*
|
||||
* Copyright (C) 2021 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.
|
||||
* <p>
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.providers
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
|
||||
class ScopedStorageProvider(
|
||||
rootFolderName: String,
|
||||
private val context: Context
|
||||
) : LocalStorageProvider(rootFolderName) {
|
||||
|
||||
override fun getPrimaryStorageDirectory(): File = context.filesDir
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.providers
|
||||
|
||||
interface SharedPreferencesProvider {
|
||||
|
||||
fun putString(key: String, value: String)
|
||||
fun getString(key: String, defaultValue: String?): String?
|
||||
|
||||
fun putInt(key: String, value: Int)
|
||||
fun getInt(key: String, defaultValue: Int): Int
|
||||
|
||||
fun putLong(key: String, value: Long)
|
||||
fun getLong(key: String, defaultValue: Long): Long
|
||||
|
||||
fun putBoolean(key: String, value: Boolean)
|
||||
fun getBoolean(key: String, defaultValue: Boolean): Boolean
|
||||
|
||||
fun containsPreference(key: String): Boolean
|
||||
|
||||
fun removePreference(key: String)
|
||||
|
||||
fun contains(key: String): Boolean
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.providers.implementation
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.preference.PreferenceManager
|
||||
import eu.qsfera.android.data.providers.SharedPreferencesProvider
|
||||
|
||||
class OCSharedPreferencesProvider(
|
||||
context: Context
|
||||
) : SharedPreferencesProvider {
|
||||
|
||||
// To do: Move to Androidx Preferences or DataStore
|
||||
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val editor = sharedPreferences.edit()
|
||||
|
||||
override fun putString(key: String, value: String) = editor.putString(key, value).apply()
|
||||
override fun getString(key: String, defaultValue: String?) = sharedPreferences.getString(key, defaultValue)
|
||||
|
||||
override fun putInt(key: String, value: Int) = editor.putInt(key, value).apply()
|
||||
override fun getInt(key: String, defaultValue: Int) = sharedPreferences.getInt(key, defaultValue)
|
||||
|
||||
override fun putLong(key: String, value: Long) = editor.putLong(key, value).apply()
|
||||
override fun getLong(key: String, defaultValue: Long) = sharedPreferences.getLong(key, defaultValue)
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) = editor.putBoolean(key, value).apply()
|
||||
override fun getBoolean(key: String, defaultValue: Boolean) = sharedPreferences.getBoolean(key, defaultValue)
|
||||
|
||||
override fun containsPreference(key: String) = sharedPreferences.contains(key)
|
||||
|
||||
override fun removePreference(key: String) = editor.remove(key).apply()
|
||||
|
||||
override fun contains(key: String) = sharedPreferences.contains(key)
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.server.datasources
|
||||
|
||||
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||
|
||||
interface RemoteServerInfoDataSource {
|
||||
fun getServerInfo(path: String, enforceOIDC: Boolean): ServerInfo
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.server.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.server.datasources.RemoteServerInfoDataSource
|
||||
import eu.qsfera.android.domain.exceptions.QSferaVersionNotSupportedException
|
||||
import eu.qsfera.android.domain.exceptions.SpecificServiceUnavailableException
|
||||
import eu.qsfera.android.domain.server.model.AuthenticationMethod
|
||||
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||
import eu.qsfera.android.lib.common.http.HttpConstants
|
||||
import eu.qsfera.android.lib.common.network.WebdavUtils.normalizeProtocolPrefix
|
||||
import eu.qsfera.android.lib.resources.status.RemoteServerInfo
|
||||
import eu.qsfera.android.lib.resources.status.services.ServerInfoService
|
||||
|
||||
class OCRemoteServerInfoDataSource(
|
||||
private val serverInfoService: ServerInfoService,
|
||||
private val clientManager: ClientManager
|
||||
) : RemoteServerInfoDataSource {
|
||||
|
||||
// Basically, tries to access to the root folder without authorization and analyzes the response.
|
||||
fun getAuthenticationMethod(path: String): AuthenticationMethod {
|
||||
// Use the same client across the whole login process to keep cookies updated.
|
||||
val qsferaClient = clientManager.getClientForAnonymousCredentials(path, false)
|
||||
|
||||
// Step 1: Check whether the root folder exists.
|
||||
val checkPathExistenceResult =
|
||||
serverInfoService.checkPathExistence(path = path, isUserLoggedIn = false, client = qsferaClient)
|
||||
|
||||
// Step 2: Check if server is available (If server is in maintenance for example, throw exception with specific message)
|
||||
if (checkPathExistenceResult.httpCode == HttpConstants.HTTP_SERVICE_UNAVAILABLE) {
|
||||
throw SpecificServiceUnavailableException(checkPathExistenceResult.httpPhrase)
|
||||
}
|
||||
|
||||
// Step 3: look for authentication methods
|
||||
var authenticationMethod = AuthenticationMethod.NONE
|
||||
if (checkPathExistenceResult.httpCode == HttpConstants.HTTP_UNAUTHORIZED) {
|
||||
val authenticateHeaders = checkPathExistenceResult.authenticateHeaders
|
||||
var isBasic = false
|
||||
authenticateHeaders.forEach { authenticateHeader ->
|
||||
if (authenticateHeader.contains(AuthenticationMethod.BEARER_TOKEN.toString())) {
|
||||
return AuthenticationMethod.BEARER_TOKEN // Bearer top priority
|
||||
} else if (authenticateHeader.contains(AuthenticationMethod.BASIC_HTTP_AUTH.toString())) {
|
||||
isBasic = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isBasic) {
|
||||
authenticationMethod = AuthenticationMethod.BASIC_HTTP_AUTH
|
||||
}
|
||||
}
|
||||
|
||||
return authenticationMethod
|
||||
}
|
||||
|
||||
fun getRemoteStatus(path: String): RemoteServerInfo {
|
||||
val qsferaClient = clientManager.getClientForAnonymousCredentials(path, true)
|
||||
|
||||
val remoteStatusResult = serverInfoService.getRemoteStatus(path, qsferaClient)
|
||||
|
||||
val remoteServerInfo = executeRemoteOperation {
|
||||
remoteStatusResult
|
||||
}
|
||||
|
||||
if (!remoteServerInfo.qsferaVersion.isServerVersionSupported && !remoteServerInfo.qsferaVersion.isVersionHidden) {
|
||||
throw QSferaVersionNotSupportedException()
|
||||
}
|
||||
|
||||
return remoteServerInfo
|
||||
}
|
||||
|
||||
override fun getServerInfo(path: String, enforceOIDC: Boolean): ServerInfo {
|
||||
// First step: check the status of the server (including its version)
|
||||
val remoteServerInfo = getRemoteStatus(path)
|
||||
val normalizedProtocolPrefix =
|
||||
normalizeProtocolPrefix(remoteServerInfo.baseUrl, remoteServerInfo.isSecureConnection)
|
||||
|
||||
// Second step: get authentication method required by the server
|
||||
val authenticationMethod =
|
||||
if (enforceOIDC) AuthenticationMethod.BEARER_TOKEN
|
||||
else getAuthenticationMethod(normalizedProtocolPrefix)
|
||||
|
||||
return if (authenticationMethod == AuthenticationMethod.BEARER_TOKEN) {
|
||||
ServerInfo.OAuth2Server(
|
||||
qsferaVersion = remoteServerInfo.qsferaVersion.version,
|
||||
baseUrl = normalizedProtocolPrefix
|
||||
)
|
||||
} else {
|
||||
ServerInfo.BasicServer(
|
||||
qsferaVersion = remoteServerInfo.qsferaVersion.version,
|
||||
baseUrl = normalizedProtocolPrefix,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.server.repository
|
||||
|
||||
import eu.qsfera.android.data.oauth.datasources.RemoteOAuthDataSource
|
||||
import eu.qsfera.android.data.server.datasources.RemoteServerInfoDataSource
|
||||
import eu.qsfera.android.data.webfinger.datasources.RemoteWebFingerDataSource
|
||||
import eu.qsfera.android.domain.server.ServerInfoRepository
|
||||
import eu.qsfera.android.domain.server.model.ServerInfo
|
||||
import eu.qsfera.android.domain.webfinger.model.WebFingerOidcInfo
|
||||
import eu.qsfera.android.domain.webfinger.model.WebFingerRel
|
||||
import timber.log.Timber
|
||||
|
||||
class OCServerInfoRepository(
|
||||
private val remoteServerInfoDataSource: RemoteServerInfoDataSource,
|
||||
private val webFingerDatasource: RemoteWebFingerDataSource,
|
||||
private val oidcRemoteOAuthDataSource: RemoteOAuthDataSource,
|
||||
) : ServerInfoRepository {
|
||||
|
||||
override fun getServerInfo(path: String, creatingAccount: Boolean, enforceOIDC: Boolean): ServerInfo {
|
||||
// Try webfinger first to get OIDC client config (client_id, scopes) and issuer.
|
||||
// This is a lightweight call that returns null if webfinger is not available.
|
||||
val oidcInfoFromWebFinger: WebFingerOidcInfo? = retrieveOidcInfoFromWebFinger(serverUrl = path)
|
||||
|
||||
// Always check server status to get the proper baseUrl and version.
|
||||
// We must not skip this, because the server may normalize/redirect the URL,
|
||||
// and the account name is built from baseUrl + username.
|
||||
val serverInfo = remoteServerInfoDataSource.getServerInfo(path, enforceOIDC)
|
||||
|
||||
return if (serverInfo is ServerInfo.BasicServer) {
|
||||
serverInfo
|
||||
} else {
|
||||
// OIDC discovery: prefer the webfinger issuer if available (it may differ from baseUrl),
|
||||
// otherwise discover from the server's baseUrl.
|
||||
val oidcDiscoveryUrl = oidcInfoFromWebFinger?.issuer ?: serverInfo.baseUrl
|
||||
val openIDConnectServerConfiguration = try {
|
||||
oidcRemoteOAuthDataSource.performOIDCDiscovery(oidcDiscoveryUrl)
|
||||
} catch (exception: Exception) {
|
||||
Timber.d(exception, "OIDC discovery not found")
|
||||
null
|
||||
}
|
||||
|
||||
if (openIDConnectServerConfiguration != null) {
|
||||
ServerInfo.OIDCServer(
|
||||
qsferaVersion = serverInfo.qsferaVersion,
|
||||
baseUrl = serverInfo.baseUrl,
|
||||
oidcServerConfiguration = openIDConnectServerConfiguration,
|
||||
webFingerClientId = oidcInfoFromWebFinger?.clientId,
|
||||
webFingerScopes = oidcInfoFromWebFinger?.scopes,
|
||||
)
|
||||
} else {
|
||||
ServerInfo.OAuth2Server(
|
||||
qsferaVersion = serverInfo.qsferaVersion,
|
||||
baseUrl = serverInfo.baseUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrieveOidcInfoFromWebFinger(
|
||||
serverUrl: String,
|
||||
): WebFingerOidcInfo? = try {
|
||||
webFingerDatasource.getOidcInfoFromWebFinger(
|
||||
lookupServer = serverUrl,
|
||||
rel = WebFingerRel.OIDC_ISSUER_DISCOVERY,
|
||||
resource = serverUrl,
|
||||
)
|
||||
} catch (exception: Exception) {
|
||||
Timber.d(exception, "Cant retrieve the oidc info from webfinger.")
|
||||
null
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.sharees.datasources
|
||||
|
||||
import eu.qsfera.android.domain.sharing.sharees.model.OCSharee
|
||||
|
||||
interface RemoteShareeDataSource {
|
||||
fun getSharees(
|
||||
searchString: String,
|
||||
page: Int,
|
||||
perPage: Int,
|
||||
accountName: String,
|
||||
): List<OCSharee>
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.sharees.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.sharing.sharees.datasources.RemoteShareeDataSource
|
||||
import eu.qsfera.android.data.sharing.sharees.datasources.mapper.RemoteShareeMapper
|
||||
import eu.qsfera.android.domain.sharing.sharees.model.OCSharee
|
||||
|
||||
class OCRemoteShareeDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
private val shareeMapper: RemoteShareeMapper
|
||||
) : RemoteShareeDataSource {
|
||||
|
||||
override fun getSharees(
|
||||
searchString: String,
|
||||
page: Int,
|
||||
perPage: Int,
|
||||
accountName: String,
|
||||
): List<OCSharee> =
|
||||
executeRemoteOperation {
|
||||
clientManager.getShareeService(accountName)
|
||||
.getSharees(
|
||||
searchString = searchString,
|
||||
page = page,
|
||||
perPage = perPage
|
||||
)
|
||||
}.let {
|
||||
shareeMapper.toModel(it)
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Christian Schabesberger
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.sharees.datasources.mapper
|
||||
|
||||
import eu.qsfera.android.domain.mappers.RemoteMapper
|
||||
import eu.qsfera.android.domain.sharing.sharees.model.OCSharee
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
import eu.qsfera.android.lib.resources.shares.responses.ShareeItem
|
||||
import eu.qsfera.android.lib.resources.shares.responses.ShareeOcsResponse
|
||||
|
||||
class RemoteShareeMapper : RemoteMapper<List<OCSharee>, ShareeOcsResponse> {
|
||||
private fun mapShareeItemToOCSharee(item: ShareeItem, isExactMatch: Boolean) =
|
||||
OCSharee(
|
||||
label = item.label,
|
||||
shareType = ShareType.fromValue(item.value.shareType)!!,
|
||||
shareWith = item.value.shareWith,
|
||||
additionalInfo = item.value.additionalInfo ?: "",
|
||||
isExactMatch = isExactMatch
|
||||
)
|
||||
|
||||
override fun toModel(remote: ShareeOcsResponse?): List<OCSharee> {
|
||||
val exactMatches = remote?.exact?.getFlatRepresentation()?.map {
|
||||
mapShareeItemToOCSharee(it, isExactMatch = true)
|
||||
}
|
||||
val nonExactMatches = remote?.getFlatRepresentationWithoutExact()?.map {
|
||||
mapShareeItemToOCSharee(it, isExactMatch = false)
|
||||
}
|
||||
return ArrayList<OCSharee>().apply {
|
||||
if (exactMatches != null) {
|
||||
addAll(exactMatches)
|
||||
}
|
||||
if (nonExactMatches != null) {
|
||||
addAll(nonExactMatches)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// not needed
|
||||
override fun toRemote(model: List<OCSharee>?): ShareeOcsResponse? = null
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.sharees.repository
|
||||
|
||||
import eu.qsfera.android.data.sharing.sharees.datasources.RemoteShareeDataSource
|
||||
import eu.qsfera.android.domain.sharing.sharees.ShareeRepository
|
||||
import eu.qsfera.android.domain.sharing.sharees.model.OCSharee
|
||||
|
||||
class OCShareeRepository(
|
||||
private val remoteShareeDataSource: RemoteShareeDataSource
|
||||
) : ShareeRepository {
|
||||
|
||||
override fun getSharees(
|
||||
searchString: String,
|
||||
page: Int,
|
||||
perPage: Int,
|
||||
accountName: String,
|
||||
): List<OCSharee> = remoteShareeDataSource.getSharees(
|
||||
searchString = searchString,
|
||||
page = page,
|
||||
perPage = perPage,
|
||||
accountName = accountName,
|
||||
)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.datasources
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
|
||||
interface LocalShareDataSource {
|
||||
fun getSharesAsLiveData(
|
||||
filePath: String,
|
||||
accountName: String,
|
||||
shareTypes: List<ShareType>
|
||||
): LiveData<List<OCShare>>
|
||||
|
||||
fun getShareAsLiveData(
|
||||
remoteId: String
|
||||
): LiveData<OCShare>
|
||||
|
||||
fun insert(ocShare: OCShare): Long
|
||||
|
||||
fun insert(ocShares: List<OCShare>): List<Long>
|
||||
|
||||
fun update(ocShare: OCShare): Long
|
||||
|
||||
fun replaceShares(ocShares: List<OCShare>): List<Long>
|
||||
|
||||
fun deleteShare(remoteId: String): Int
|
||||
|
||||
fun deleteSharesForFile(filePath: String, accountName: String)
|
||||
|
||||
fun deleteSharesForAccount(accountName: String)
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.datasources
|
||||
|
||||
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
import eu.qsfera.android.lib.resources.shares.RemoteShare.Companion.INIT_EXPIRATION_DATE_IN_MILLIS
|
||||
|
||||
interface RemoteShareDataSource {
|
||||
fun getShares(
|
||||
remoteFilePath: String,
|
||||
reshares: Boolean,
|
||||
subfiles: Boolean,
|
||||
accountName: String
|
||||
): List<OCShare>
|
||||
|
||||
fun insert(
|
||||
remoteFilePath: String,
|
||||
shareType: ShareType,
|
||||
shareWith: String,
|
||||
permissions: Int,
|
||||
name: String = "",
|
||||
password: String = "",
|
||||
expirationDate: Long = INIT_EXPIRATION_DATE_IN_MILLIS,
|
||||
accountName: String
|
||||
): OCShare
|
||||
|
||||
fun updateShare(
|
||||
remoteId: String,
|
||||
name: String = "",
|
||||
password: String? = "",
|
||||
expirationDateInMillis: Long = INIT_EXPIRATION_DATE_IN_MILLIS,
|
||||
permissions: Int,
|
||||
accountName: String
|
||||
): OCShare
|
||||
|
||||
fun deleteShare(
|
||||
remoteId: String,
|
||||
accountName: String,
|
||||
)
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.map
|
||||
import eu.qsfera.android.data.sharing.shares.datasources.LocalShareDataSource
|
||||
import eu.qsfera.android.data.sharing.shares.db.OCShareDao
|
||||
import eu.qsfera.android.data.sharing.shares.db.OCShareEntity
|
||||
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
|
||||
class OCLocalShareDataSource(
|
||||
private val ocShareDao: OCShareDao,
|
||||
) : LocalShareDataSource {
|
||||
|
||||
override fun getSharesAsLiveData(
|
||||
filePath: String,
|
||||
accountName: String,
|
||||
shareTypes: List<ShareType>
|
||||
): LiveData<List<OCShare>> =
|
||||
ocShareDao.getSharesAsLiveData(
|
||||
filePath,
|
||||
accountName,
|
||||
shareTypes.map { it.value },
|
||||
).map { ocShareEntities ->
|
||||
ocShareEntities.map { ocShareEntity -> ocShareEntity.toModel() }
|
||||
}
|
||||
|
||||
override fun getShareAsLiveData(remoteId: String): LiveData<OCShare> =
|
||||
ocShareDao.getShareAsLiveData(remoteId).map { ocShareEntity ->
|
||||
ocShareEntity.toModel()
|
||||
}
|
||||
|
||||
override fun insert(ocShare: OCShare): Long =
|
||||
ocShareDao.insertOrReplace(
|
||||
ocShare.toEntity()
|
||||
)
|
||||
|
||||
override fun insert(ocShares: List<OCShare>): List<Long> =
|
||||
ocShareDao.insertOrReplace(
|
||||
ocShares.map { ocShare -> ocShare.toEntity() }
|
||||
)
|
||||
|
||||
override fun update(ocShare: OCShare): Long = ocShareDao.update(ocShare.toEntity())
|
||||
|
||||
override fun replaceShares(ocShares: List<OCShare>): List<Long> =
|
||||
ocShareDao.replaceShares(
|
||||
ocShares.map { ocShare -> ocShare.toEntity() }
|
||||
)
|
||||
|
||||
override fun deleteShare(remoteId: String): Int = ocShareDao.deleteShare(remoteId)
|
||||
|
||||
override fun deleteSharesForFile(filePath: String, accountName: String) =
|
||||
ocShareDao.deleteSharesForFile(filePath, accountName)
|
||||
|
||||
override fun deleteSharesForAccount(accountName: String) =
|
||||
ocShareDao.deleteSharesForAccount(accountName)
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun OCShareEntity.toModel(): OCShare =
|
||||
OCShare(
|
||||
id = id,
|
||||
shareType = ShareType.fromValue(shareType)!!,
|
||||
shareWith = shareWith,
|
||||
path = path,
|
||||
permissions = permissions,
|
||||
sharedDate = sharedDate,
|
||||
expirationDate = expirationDate,
|
||||
token = token,
|
||||
sharedWithDisplayName = sharedWithDisplayName,
|
||||
sharedWithAdditionalInfo = sharedWithAdditionalInfo,
|
||||
isFolder = isFolder,
|
||||
remoteId = remoteId,
|
||||
accountOwner = accountOwner,
|
||||
name = name,
|
||||
shareLink = shareLink
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun OCShare.toEntity(): OCShareEntity =
|
||||
OCShareEntity(
|
||||
shareType = shareType.value,
|
||||
shareWith = shareWith,
|
||||
path = path,
|
||||
permissions = permissions,
|
||||
sharedDate = sharedDate,
|
||||
expirationDate = expirationDate,
|
||||
token = token,
|
||||
sharedWithDisplayName = sharedWithDisplayName,
|
||||
sharedWithAdditionalInfo = sharedWithAdditionalInfo,
|
||||
isFolder = isFolder,
|
||||
remoteId = remoteId,
|
||||
accountOwner = accountOwner,
|
||||
name = name,
|
||||
shareLink = shareLink
|
||||
)
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.sharing.shares.datasources.RemoteShareDataSource
|
||||
import eu.qsfera.android.data.sharing.shares.datasources.mapper.RemoteShareMapper
|
||||
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
|
||||
class OCRemoteShareDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
private val remoteShareMapper: RemoteShareMapper
|
||||
) : RemoteShareDataSource {
|
||||
|
||||
override fun getShares(
|
||||
remoteFilePath: String,
|
||||
reshares: Boolean,
|
||||
subfiles: Boolean,
|
||||
accountName: String
|
||||
): List<OCShare> {
|
||||
executeRemoteOperation {
|
||||
clientManager.getShareService(accountName).getShares(remoteFilePath, reshares, subfiles)
|
||||
}.let {
|
||||
return it.shares.map { remoteShare ->
|
||||
remoteShareMapper.toModel(remoteShare)!!.apply {
|
||||
accountOwner = accountName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun insert(
|
||||
remoteFilePath: String,
|
||||
shareType: ShareType,
|
||||
shareWith: String,
|
||||
permissions: Int,
|
||||
name: String,
|
||||
password: String,
|
||||
expirationDate: Long,
|
||||
accountName: String
|
||||
): OCShare {
|
||||
executeRemoteOperation {
|
||||
clientManager.getShareService(accountName).insertShare(
|
||||
remoteFilePath,
|
||||
eu.qsfera.android.lib.resources.shares.ShareType.fromValue(shareType.value)!!,
|
||||
shareWith,
|
||||
permissions,
|
||||
name,
|
||||
password,
|
||||
expirationDate,
|
||||
)
|
||||
}.let {
|
||||
return remoteShareMapper.toModel(it.shares.first())!!.apply {
|
||||
accountOwner = accountName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateShare(
|
||||
remoteId: String,
|
||||
name: String,
|
||||
password: String?,
|
||||
expirationDateInMillis: Long,
|
||||
permissions: Int,
|
||||
accountName: String
|
||||
): OCShare {
|
||||
executeRemoteOperation {
|
||||
clientManager.getShareService(accountName).updateShare(
|
||||
remoteId,
|
||||
name,
|
||||
password,
|
||||
expirationDateInMillis,
|
||||
permissions,
|
||||
)
|
||||
}.let {
|
||||
return remoteShareMapper.toModel(it.shares.first())!!.apply {
|
||||
accountOwner = accountName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteShare(remoteId: String, accountName: String) {
|
||||
executeRemoteOperation {
|
||||
clientManager.getShareService(accountName).deleteShare(remoteId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.datasources.mapper
|
||||
|
||||
import eu.qsfera.android.domain.mappers.RemoteMapper
|
||||
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
import eu.qsfera.android.lib.resources.shares.RemoteShare
|
||||
import eu.qsfera.android.lib.resources.shares.ShareType as RemoteShareType
|
||||
|
||||
class RemoteShareMapper : RemoteMapper<OCShare, RemoteShare> {
|
||||
override fun toModel(remote: RemoteShare?): OCShare? =
|
||||
remote?.let {
|
||||
OCShare(
|
||||
shareType = ShareType.fromValue(remote.shareType!!.value)!!,
|
||||
shareWith = remote.shareWith,
|
||||
path = remote.path,
|
||||
permissions = remote.permissions,
|
||||
sharedDate = remote.sharedDate,
|
||||
expirationDate = remote.expirationDate,
|
||||
token = remote.token,
|
||||
sharedWithDisplayName = remote.sharedWithDisplayName,
|
||||
sharedWithAdditionalInfo = remote.sharedWithAdditionalInfo,
|
||||
isFolder = remote.isFolder,
|
||||
remoteId = remote.id,
|
||||
name = remote.name,
|
||||
shareLink = remote.shareLink
|
||||
)
|
||||
}
|
||||
|
||||
override fun toRemote(model: OCShare?): RemoteShare? =
|
||||
model?.let {
|
||||
RemoteShare(
|
||||
id = model.remoteId,
|
||||
shareWith = model.shareWith!!,
|
||||
path = model.path,
|
||||
token = model.token!!,
|
||||
sharedWithDisplayName = model.sharedWithDisplayName!!,
|
||||
sharedWithAdditionalInfo = model.sharedWithAdditionalInfo!!,
|
||||
name = model.name!!,
|
||||
shareLink = model.shareLink!!,
|
||||
shareType = RemoteShareType.fromValue(model.shareType.value),
|
||||
permissions = model.permissions,
|
||||
sharedDate = model.sharedDate,
|
||||
expirationDate = model.expirationDate,
|
||||
isFolder = model.isFolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.db
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
@Dao
|
||||
interface OCShareDao {
|
||||
@Query(SELECT_SHARE_BY_ID)
|
||||
fun getShareAsLiveData(
|
||||
remoteId: String
|
||||
): LiveData<OCShareEntity>
|
||||
|
||||
@Query(SELECT_SHARES_BY_FILEPATH_ACCOUNTOWNER_AND_TYPE)
|
||||
fun getSharesAsLiveData(
|
||||
filePath: String, accountOwner: String, shareTypes: List<Int>
|
||||
): LiveData<List<OCShareEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(ocShare: OCShareEntity): Long
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(ocShares: List<OCShareEntity>): List<Long>
|
||||
|
||||
@Transaction
|
||||
fun update(ocShare: OCShareEntity): Long {
|
||||
deleteShare(ocShare.remoteId)
|
||||
return insertOrReplace(ocShare)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
fun replaceShares(ocShares: List<OCShareEntity>): List<Long> {
|
||||
for (ocShare in ocShares) {
|
||||
deleteSharesForFile(ocShare.path, ocShare.accountOwner)
|
||||
}
|
||||
return insertOrReplace(ocShares)
|
||||
}
|
||||
|
||||
@Query(DELETE_SHARE_BY_ID)
|
||||
fun deleteShare(remoteId: String): Int
|
||||
|
||||
@Query(DELETE_SHARES_FOR_FILE)
|
||||
fun deleteSharesForFile(filePath: String, accountOwner: String)
|
||||
|
||||
@Query(DELETE_SHARES_FOR_ACCOUNT)
|
||||
fun deleteSharesForAccount(accountName: String)
|
||||
|
||||
companion object {
|
||||
private const val SELECT_SHARE_BY_ID = """
|
||||
SELECT *
|
||||
FROM ${ProviderTableMeta.OCSHARES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED} = :remoteId
|
||||
"""
|
||||
|
||||
private const val SELECT_SHARES_BY_FILEPATH_ACCOUNTOWNER_AND_TYPE = """
|
||||
SELECT *
|
||||
FROM ${ProviderTableMeta.OCSHARES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.OCSHARES_PATH} = :filePath AND
|
||||
${ProviderTableMeta.OCSHARES_ACCOUNT_OWNER} = :accountOwner AND
|
||||
${ProviderTableMeta.OCSHARES_SHARE_TYPE} IN (:shareTypes)
|
||||
"""
|
||||
|
||||
private const val DELETE_SHARE_BY_ID = """
|
||||
DELETE
|
||||
FROM ${ProviderTableMeta.OCSHARES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED} = :remoteId
|
||||
"""
|
||||
|
||||
private const val DELETE_SHARES_FOR_FILE = """
|
||||
DELETE
|
||||
FROM ${ProviderTableMeta.OCSHARES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.OCSHARES_PATH} = :filePath AND
|
||||
${ProviderTableMeta.OCSHARES_ACCOUNT_OWNER} = :accountOwner
|
||||
"""
|
||||
|
||||
private const val DELETE_SHARES_FOR_ACCOUNT = """
|
||||
DELETE
|
||||
FROM ${ProviderTableMeta.OCSHARES_TABLE_NAME}
|
||||
WHERE ${ProviderTableMeta.OCSHARES_ACCOUNT_OWNER} = :accountName
|
||||
"""
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.db
|
||||
|
||||
import android.content.ContentValues
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta
|
||||
|
||||
/**
|
||||
* Represents one record of the Shares table.
|
||||
*/
|
||||
@Entity(tableName = ProviderTableMeta.OCSHARES_TABLE_NAME)
|
||||
data class OCShareEntity(
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_TYPE)
|
||||
val shareType: Int,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_WITH)
|
||||
val shareWith: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_PATH)
|
||||
val path: String,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_PERMISSIONS)
|
||||
val permissions: Int,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARED_DATE)
|
||||
val sharedDate: Long,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_EXPIRATION_DATE)
|
||||
val expirationDate: Long,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_TOKEN)
|
||||
val token: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME)
|
||||
val sharedWithDisplayName: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_SHARE_WITH_ADDITIONAL_INFO)
|
||||
val sharedWithAdditionalInfo: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_IS_DIRECTORY)
|
||||
val isFolder: Boolean,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED)
|
||||
val remoteId: String,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ACCOUNT_OWNER)
|
||||
var accountOwner: String = "",
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_NAME)
|
||||
val name: String?,
|
||||
@ColumnInfo(name = ProviderTableMeta.OCSHARES_URL)
|
||||
val shareLink: String?
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true) var id: Int = 0
|
||||
|
||||
companion object {
|
||||
fun fromContentValues(values: ContentValues): OCShareEntity =
|
||||
OCShareEntity(
|
||||
values.getAsInteger(ProviderTableMeta.OCSHARES_SHARE_TYPE),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_SHARE_WITH),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_PATH),
|
||||
values.getAsInteger(ProviderTableMeta.OCSHARES_PERMISSIONS),
|
||||
values.getAsLong(ProviderTableMeta.OCSHARES_SHARED_DATE),
|
||||
values.getAsLong(ProviderTableMeta.OCSHARES_EXPIRATION_DATE),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_TOKEN),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_SHARE_WITH_DISPLAY_NAME),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_SHARE_WITH_ADDITIONAL_INFO),
|
||||
values.getAsBoolean(ProviderTableMeta.OCSHARES_IS_DIRECTORY),
|
||||
values.getAsLong(ProviderTableMeta.OCSHARES_ID_REMOTE_SHARED).toString(),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_ACCOUNT_OWNER),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_NAME),
|
||||
values.getAsString(ProviderTableMeta.OCSHARES_URL)
|
||||
)
|
||||
}
|
||||
}
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author David González Verdugo
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.sharing.shares.repository
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import eu.qsfera.android.data.sharing.shares.datasources.LocalShareDataSource
|
||||
import eu.qsfera.android.data.sharing.shares.datasources.RemoteShareDataSource
|
||||
import eu.qsfera.android.domain.sharing.shares.ShareRepository
|
||||
import eu.qsfera.android.domain.sharing.shares.model.OCShare
|
||||
import eu.qsfera.android.domain.sharing.shares.model.ShareType
|
||||
import eu.qsfera.android.lib.resources.shares.RemoteShare
|
||||
|
||||
class OCShareRepository(
|
||||
private val localShareDataSource: LocalShareDataSource,
|
||||
private val remoteShareDataSource: RemoteShareDataSource
|
||||
) : ShareRepository {
|
||||
|
||||
/******************************************************************************************************
|
||||
******************************************* PRIVATE SHARES *******************************************
|
||||
******************************************************************************************************/
|
||||
|
||||
override fun insertPrivateShare(
|
||||
filePath: String,
|
||||
shareType: ShareType,
|
||||
shareeName: String, // User or group name of the target sharee.
|
||||
permissions: Int,
|
||||
accountName: String
|
||||
) {
|
||||
insertShare(
|
||||
filePath = filePath,
|
||||
shareType = shareType,
|
||||
shareWith = shareeName,
|
||||
permissions = permissions,
|
||||
accountName = accountName
|
||||
)
|
||||
}
|
||||
|
||||
override fun updatePrivateShare(remoteId: String, permissions: Int, accountName: String) =
|
||||
updateShare(
|
||||
remoteId = remoteId,
|
||||
permissions = permissions,
|
||||
accountName = accountName
|
||||
)
|
||||
|
||||
/******************************************************************************************************
|
||||
******************************************* PUBLIC SHARES ********************************************
|
||||
******************************************************************************************************/
|
||||
|
||||
override fun insertPublicShare(
|
||||
filePath: String,
|
||||
permissions: Int,
|
||||
name: String,
|
||||
password: String,
|
||||
expirationTimeInMillis: Long,
|
||||
accountName: String
|
||||
) {
|
||||
insertShare(
|
||||
filePath = filePath,
|
||||
shareType = ShareType.PUBLIC_LINK,
|
||||
permissions = permissions,
|
||||
name = name,
|
||||
password = password,
|
||||
expirationTimeInMillis = expirationTimeInMillis,
|
||||
accountName = accountName
|
||||
)
|
||||
}
|
||||
|
||||
override fun updatePublicShare(
|
||||
remoteId: String,
|
||||
name: String,
|
||||
password: String?,
|
||||
expirationDateInMillis: Long,
|
||||
permissions: Int,
|
||||
accountName: String
|
||||
) =
|
||||
updateShare(
|
||||
remoteId,
|
||||
permissions,
|
||||
name,
|
||||
password,
|
||||
expirationDateInMillis,
|
||||
accountName
|
||||
)
|
||||
|
||||
/******************************************************************************************************
|
||||
*********************************************** COMMON ***********************************************
|
||||
******************************************************************************************************/
|
||||
|
||||
override fun getSharesAsLiveData(filePath: String, accountName: String): LiveData<List<OCShare>> =
|
||||
localShareDataSource.getSharesAsLiveData(
|
||||
filePath, accountName, listOf(
|
||||
ShareType.PUBLIC_LINK,
|
||||
ShareType.USER,
|
||||
ShareType.GROUP,
|
||||
ShareType.FEDERATED
|
||||
)
|
||||
)
|
||||
|
||||
override fun getShareAsLiveData(remoteId: String): LiveData<OCShare> =
|
||||
localShareDataSource.getShareAsLiveData(remoteId)
|
||||
|
||||
override fun refreshSharesFromNetwork(
|
||||
filePath: String,
|
||||
accountName: String
|
||||
) {
|
||||
remoteShareDataSource.getShares(
|
||||
filePath,
|
||||
reshares = true,
|
||||
subfiles = false,
|
||||
accountName = accountName
|
||||
).also { sharesFromNetwork ->
|
||||
if (sharesFromNetwork.isEmpty()) {
|
||||
localShareDataSource.deleteSharesForFile(filePath, accountName)
|
||||
}
|
||||
// Save shares
|
||||
localShareDataSource.replaceShares(sharesFromNetwork)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteShare(remoteId: String, accountName: String) {
|
||||
remoteShareDataSource.deleteShare(remoteId, accountName)
|
||||
localShareDataSource.deleteShare(remoteId)
|
||||
}
|
||||
|
||||
private fun insertShare(
|
||||
filePath: String,
|
||||
shareType: ShareType,
|
||||
shareWith: String = "",
|
||||
permissions: Int,
|
||||
name: String = "",
|
||||
password: String = "",
|
||||
expirationTimeInMillis: Long = RemoteShare.INIT_EXPIRATION_DATE_IN_MILLIS,
|
||||
accountName: String
|
||||
) {
|
||||
remoteShareDataSource.insert(
|
||||
filePath,
|
||||
shareType,
|
||||
shareWith,
|
||||
permissions,
|
||||
name,
|
||||
password,
|
||||
expirationTimeInMillis,
|
||||
accountName
|
||||
).also { remotelyInsertedShare ->
|
||||
localShareDataSource.insert(remotelyInsertedShare)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateShare(
|
||||
remoteId: String,
|
||||
permissions: Int,
|
||||
name: String = "",
|
||||
password: String? = "",
|
||||
expirationDateInMillis: Long = RemoteShare.INIT_EXPIRATION_DATE_IN_MILLIS,
|
||||
accountName: String
|
||||
) {
|
||||
remoteShareDataSource.updateShare(
|
||||
remoteId,
|
||||
name,
|
||||
password,
|
||||
expirationDateInMillis,
|
||||
permissions,
|
||||
accountName
|
||||
).also { remotelyUpdatedShare ->
|
||||
localShareDataSource.update(remotelyUpdatedShare)
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.spaces.datasources
|
||||
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalSpacesDataSource {
|
||||
fun saveSpacesForAccount(listOfSpaces: List<OCSpace>)
|
||||
fun getPersonalSpaceForAccount(accountName: String): OCSpace?
|
||||
fun getSharesSpaceForAccount(accountName: String): OCSpace?
|
||||
fun getSpacesFromEveryAccountAsStream(): Flow<List<OCSpace>>
|
||||
fun getSpacesByDriveTypeWithSpecialsForAccountAsFlow(accountName: String, filterDriveTypes: Set<String>): Flow<List<OCSpace>>
|
||||
fun getPersonalAndProjectSpacesForAccount(accountName: String): List<OCSpace>
|
||||
fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String): OCSpace
|
||||
fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace?
|
||||
fun getWebDavUrlForSpace(spaceId: String?, accountName: String): String?
|
||||
fun deleteSpacesForAccount(accountName: String)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* qsfera 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.spaces.datasources
|
||||
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace
|
||||
|
||||
interface RemoteSpacesDataSource {
|
||||
fun refreshSpacesForAccount(accountName: String): List<OCSpace>
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.spaces.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.spaces.datasources.LocalSpacesDataSource
|
||||
import eu.qsfera.android.data.spaces.db.SpaceQuotaEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpaceRootEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpacesDao
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity
|
||||
import eu.qsfera.android.data.spaces.db.SpacesWithSpecials
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace.Companion.DRIVE_TYPE_PERSONAL
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace.Companion.DRIVE_TYPE_PROJECT
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace.Companion.SPACE_ID_SHARES
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceDeleted
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceFile
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceOwner
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceQuota
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceRoot
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceSpecial
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceSpecialFolder
|
||||
import eu.qsfera.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<OCSpace>) {
|
||||
val spaceEntities = mutableListOf<SpacesEntity>()
|
||||
val spaceSpecialEntities = mutableListOf<SpaceSpecialEntity>()
|
||||
|
||||
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 getPersonalSpaceForAccount(accountName: String): OCSpace? =
|
||||
spacesDao.getSpacesByDriveTypeForAccount(
|
||||
accountName = accountName,
|
||||
filterDriveTypes = setOf(DRIVE_TYPE_PERSONAL)
|
||||
).map { it.toModel() }.firstOrNull()
|
||||
|
||||
override fun getSharesSpaceForAccount(accountName: String): OCSpace? =
|
||||
spacesDao.getSpaceByIdForAccount(spaceId = SPACE_ID_SHARES, accountName = accountName)?.toModel()
|
||||
|
||||
override fun getSpacesFromEveryAccountAsStream(): Flow<List<OCSpace>> =
|
||||
spacesDao.getSpacesByDriveTypeFromEveryAccountAsStream(
|
||||
filterDriveTypes = setOf(DRIVE_TYPE_PERSONAL, DRIVE_TYPE_PROJECT)
|
||||
).map { spaceEntities ->
|
||||
spaceEntities.map { spaceEntity -> spaceEntity.toModel() }
|
||||
}
|
||||
|
||||
override fun getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
|
||||
accountName: String,
|
||||
filterDriveTypes: Set<String>,
|
||||
): Flow<List<OCSpace>> =
|
||||
spacesDao.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
|
||||
accountName = accountName,
|
||||
filterDriveTypes = filterDriveTypes,
|
||||
).map { spacesWithSpecialsEntitiesList ->
|
||||
spacesWithSpecialsEntitiesList.map { spacesWithSpecialsEntity ->
|
||||
spacesWithSpecialsEntity.toModel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPersonalAndProjectSpacesForAccount(accountName: String): List<OCSpace> =
|
||||
spacesDao.getSpacesByDriveTypeForAccount(
|
||||
accountName = accountName,
|
||||
filterDriveTypes = setOf(DRIVE_TYPE_PERSONAL, DRIVE_TYPE_PROJECT),
|
||||
).map { it.toModel() }
|
||||
|
||||
override fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String): OCSpace =
|
||||
spacesDao.getSpaceWithSpecialsByIdForAccount(spaceId, accountName).toModel()
|
||||
|
||||
override fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? =
|
||||
spacesDao.getSpaceByIdForAccount(spaceId = spaceId, accountName = accountName)?.toModel()
|
||||
|
||||
override fun getWebDavUrlForSpace(spaceId: String?, accountName: String): String? =
|
||||
spacesDao.getWebDavUrlForSpace(spaceId, accountName)
|
||||
|
||||
override fun deleteSpacesForAccount(accountName: String) {
|
||||
spacesDao.deleteSpacesForAccount(accountName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun SpacesWithSpecials.toModel() =
|
||||
space.toModel(specials = specials)
|
||||
|
||||
@VisibleForTesting
|
||||
fun SpaceSpecialEntity.toModel() =
|
||||
SpaceSpecial(
|
||||
eTag = eTag,
|
||||
file = SpaceFile(
|
||||
mimeType = fileMimeType
|
||||
),
|
||||
id = id,
|
||||
lastModifiedDateTime = lastModifiedDateTime,
|
||||
name = name,
|
||||
size = size,
|
||||
specialFolder = SpaceSpecialFolder(
|
||||
name = specialFolderName
|
||||
),
|
||||
webDavUrl = webDavUrl
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun SpacesEntity.toModel(specials: List<SpaceSpecialEntity>? = null) =
|
||||
OCSpace(
|
||||
accountName = accountName,
|
||||
driveAlias = driveAlias,
|
||||
driveType = driveType,
|
||||
id = id,
|
||||
lastModifiedDateTime = lastModifiedDateTime,
|
||||
name = name,
|
||||
owner = ownerId?.let { spaceOwnerIdEntity ->
|
||||
SpaceOwner(
|
||||
user = SpaceUser(
|
||||
id = spaceOwnerIdEntity
|
||||
)
|
||||
)
|
||||
},
|
||||
quota = quota?.let { spaceQuotaEntity ->
|
||||
SpaceQuota(
|
||||
remaining = spaceQuotaEntity.remaining,
|
||||
state = spaceQuotaEntity.state,
|
||||
total = spaceQuotaEntity.total,
|
||||
used = spaceQuotaEntity.used,
|
||||
)
|
||||
},
|
||||
root = root.let { spaceRootEntity ->
|
||||
SpaceRoot(
|
||||
eTag = spaceRootEntity.eTag,
|
||||
id = spaceRootEntity.id,
|
||||
webDavUrl = spaceRootEntity.webDavUrl,
|
||||
deleted = spaceRootEntity.deleteState?.let { SpaceDeleted(state = it) },
|
||||
)
|
||||
},
|
||||
webUrl = webUrl,
|
||||
description = description,
|
||||
special = specials?.map { it.toModel() },
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
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,
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* qsfera 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.spaces.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.spaces.datasources.RemoteSpacesDataSource
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceDeleted
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceFile
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceOwner
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceQuota
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceRoot
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceSpecial
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceSpecialFolder
|
||||
import eu.qsfera.android.domain.spaces.model.SpaceUser
|
||||
import eu.qsfera.android.lib.resources.spaces.responses.SpaceResponse
|
||||
|
||||
class OCRemoteSpacesDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
) : RemoteSpacesDataSource {
|
||||
override fun refreshSpacesForAccount(accountName: String): List<OCSpace> {
|
||||
val spacesResponse = executeRemoteOperation {
|
||||
clientManager.getSpacesService(accountName).getSpaces()
|
||||
}
|
||||
|
||||
return spacesResponse.map { it.toModel(accountName) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@VisibleForTesting
|
||||
fun SpaceResponse.toModel(accountName: String): OCSpace =
|
||||
OCSpace(
|
||||
accountName = accountName,
|
||||
driveAlias = driveAlias,
|
||||
driveType = driveType,
|
||||
id = id,
|
||||
lastModifiedDateTime = lastModifiedDateTime,
|
||||
name = name,
|
||||
owner = owner?.let { ownerResponse ->
|
||||
SpaceOwner(
|
||||
user = SpaceUser(
|
||||
id = ownerResponse.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,
|
||||
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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.spaces.db
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ID
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_SPACE_ID
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID
|
||||
import eu.qsfera.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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.spaces.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.spaces.db.SpaceSpecialEntity.Companion.SPACES_SPECIAL_ID
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_DRIVE_TYPE
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_WEB_DAV_URL
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface SpacesDao {
|
||||
@Transaction
|
||||
fun insertOrDeleteSpaces(
|
||||
listOfSpacesEntities: List<SpacesEntity>,
|
||||
listOfSpecialEntities: List<SpaceSpecialEntity>,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
|
||||
// Delete specials that are not attached to the current spaces of the account anymore
|
||||
val currentSpecials = getAllSpecialsForAccount(currentAccountName)
|
||||
|
||||
val specialsToDelete = currentSpecials.filterNot { oldSpecial ->
|
||||
listOfSpecialEntities.any { it.id == oldSpecial.id }
|
||||
}
|
||||
|
||||
specialsToDelete.forEach { specialToDelete ->
|
||||
deleteSpecialForAccountById(accountName = specialToDelete.accountName, specialId = specialToDelete.id)
|
||||
}
|
||||
|
||||
// Upsert new spaces and specials
|
||||
upsertSpaces(listOfSpacesEntities)
|
||||
upsertSpecials(listOfSpecialEntities)
|
||||
}
|
||||
|
||||
@Upsert
|
||||
fun upsertSpaces(listOfSpacesEntities: List<SpacesEntity>)
|
||||
|
||||
@Upsert
|
||||
fun upsertSpecials(listOfSpecialEntities: List<SpaceSpecialEntity>)
|
||||
|
||||
@Query(SELECT_SPACES_BY_DRIVE_TYPE)
|
||||
fun getSpacesByDriveTypeFromEveryAccountAsStream(
|
||||
filterDriveTypes: Set<String>,
|
||||
): Flow<List<SpacesEntity>>
|
||||
|
||||
@Query(SELECT_ALL_SPACES_FOR_ACCOUNT)
|
||||
fun getAllSpacesForAccount(
|
||||
accountName: String,
|
||||
): List<SpacesEntity>
|
||||
|
||||
@Query(SELECT_SPACES_BY_DRIVE_TYPE_FOR_ACCOUNT)
|
||||
fun getSpacesByDriveTypeForAccount(
|
||||
accountName: String,
|
||||
filterDriveTypes: Set<String>,
|
||||
): List<SpacesEntity>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_SPACES_BY_DRIVE_TYPE_FOR_ACCOUNT)
|
||||
fun getSpacesByDriveTypeWithSpecialsForAccountAsFlow(
|
||||
accountName: String,
|
||||
filterDriveTypes: Set<String>,
|
||||
): Flow<List<SpacesWithSpecials>>
|
||||
|
||||
@Transaction
|
||||
@Query(SELECT_SPACE_BY_ID_FOR_ACCOUNT)
|
||||
fun getSpaceWithSpecialsByIdForAccount(
|
||||
spaceId: String?,
|
||||
accountName: String,
|
||||
): SpacesWithSpecials
|
||||
|
||||
@Query(SELECT_SPACE_BY_ID_FOR_ACCOUNT)
|
||||
fun getSpaceByIdForAccount(
|
||||
spaceId: String?,
|
||||
accountName: String,
|
||||
): SpacesEntity?
|
||||
|
||||
@Query(SELECT_ALL_SPECIALS_FOR_ACCOUNT)
|
||||
fun getAllSpecialsForAccount(
|
||||
accountName: String,
|
||||
): List<SpaceSpecialEntity>
|
||||
|
||||
@Query(SELECT_WEB_DAV_URL_FOR_SPACE)
|
||||
fun getWebDavUrlForSpace(
|
||||
spaceId: String?,
|
||||
accountName: String,
|
||||
): String?
|
||||
|
||||
@Query(DELETE_ALL_SPACES_FOR_ACCOUNT)
|
||||
fun deleteSpacesForAccount(accountName: String)
|
||||
|
||||
@Query(DELETE_SPACE_FOR_ACCOUNT_BY_ID)
|
||||
fun deleteSpaceForAccountById(accountName: String, spaceId: String)
|
||||
|
||||
@Query(DELETE_SPECIAL_FOR_ACCOUNT_BY_ID)
|
||||
fun deleteSpecialForAccountById(accountName: String, specialId: String)
|
||||
|
||||
companion object {
|
||||
private const val SELECT_SPACES_BY_DRIVE_TYPE = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}
|
||||
WHERE $SPACES_DRIVE_TYPE IN (:filterDriveTypes)
|
||||
"""
|
||||
|
||||
private const val SELECT_ALL_SPACES_FOR_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}
|
||||
WHERE $SPACES_ACCOUNT_NAME = :accountName
|
||||
"""
|
||||
|
||||
private const val SELECT_SPACES_BY_DRIVE_TYPE_FOR_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}
|
||||
WHERE $SPACES_ACCOUNT_NAME = :accountName AND ($SPACES_DRIVE_TYPE IN (:filterDriveTypes))
|
||||
ORDER BY $SPACES_DRIVE_TYPE ASC, name COLLATE NOCASE ASC
|
||||
"""
|
||||
|
||||
private const val SELECT_SPACE_BY_ID_FOR_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}
|
||||
WHERE $SPACES_ID = :spaceId AND $SPACES_ACCOUNT_NAME = :accountName
|
||||
"""
|
||||
|
||||
private const val SELECT_ALL_SPECIALS_FOR_ACCOUNT = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_SPECIAL_TABLE_NAME}
|
||||
WHERE $SPACES_SPECIAL_ACCOUNT_NAME = :accountName
|
||||
"""
|
||||
|
||||
private const val SELECT_WEB_DAV_URL_FOR_SPACE = """
|
||||
SELECT $SPACES_ROOT_WEB_DAV_URL
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_TABLE_NAME}
|
||||
WHERE $SPACES_ID = :spaceId AND $SPACES_ACCOUNT_NAME = :accountName
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
private const val DELETE_SPECIAL_FOR_ACCOUNT_BY_ID = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.SPACES_SPECIAL_TABLE_NAME}
|
||||
WHERE $SPACES_SPECIAL_ACCOUNT_NAME = :accountName AND $SPACES_SPECIAL_ID LIKE :specialId
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.spaces.db
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Relation
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ID
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_REMAINING
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_STATE
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_TOTAL
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_QUOTA_USED
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_DELETED_STATE
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_ETAG
|
||||
import eu.qsfera.android.data.spaces.db.SpacesEntity.Companion.SPACES_ROOT_ID
|
||||
import eu.qsfera.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 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<SpaceSpecialEntity>
|
||||
)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Jorge Aguado Recio
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.spaces.repository
|
||||
|
||||
import eu.qsfera.android.data.capabilities.datasources.LocalCapabilitiesDataSource
|
||||
import eu.qsfera.android.data.spaces.datasources.LocalSpacesDataSource
|
||||
import eu.qsfera.android.data.spaces.datasources.RemoteSpacesDataSource
|
||||
import eu.qsfera.android.data.user.datasources.LocalUserDataSource
|
||||
import eu.qsfera.android.domain.spaces.SpacesRepository
|
||||
import eu.qsfera.android.domain.spaces.model.OCSpace
|
||||
import eu.qsfera.android.domain.user.model.UserQuotaState
|
||||
import eu.qsfera.android.domain.user.model.UserQuota
|
||||
|
||||
class OCSpacesRepository(
|
||||
private val localSpacesDataSource: LocalSpacesDataSource,
|
||||
private val localUserDataSource: LocalUserDataSource,
|
||||
private val remoteSpacesDataSource: RemoteSpacesDataSource,
|
||||
private val localCapabilitiesDataSource: LocalCapabilitiesDataSource,
|
||||
) : SpacesRepository {
|
||||
override fun refreshSpacesForAccount(accountName: String) {
|
||||
remoteSpacesDataSource.refreshSpacesForAccount(accountName).also { listOfSpaces ->
|
||||
localSpacesDataSource.saveSpacesForAccount(listOfSpaces)
|
||||
val personalSpace = listOfSpaces.find { it.isPersonal }
|
||||
val capabilities = localCapabilitiesDataSource.getCapabilitiesForAccount(accountName)
|
||||
val isMultiPersonal = capabilities?.spaces?.hasMultiplePersonalSpaces
|
||||
val userQuota = personalSpace?.let {
|
||||
if (isMultiPersonal == true) {
|
||||
UserQuota(accountName, -4, 0, 0, UserQuotaState.NORMAL)
|
||||
} else if (it.quota?.total!!.toInt() == 0) {
|
||||
UserQuota(accountName, -3, it.quota?.used!!, it.quota?.total!!, UserQuotaState.fromValue(it.quota?.state!!))
|
||||
} else {
|
||||
UserQuota(accountName, it.quota?.remaining!!, it.quota?.used!!, it.quota?.total!!, UserQuotaState.fromValue(it.quota?.state!!))
|
||||
}
|
||||
} ?: UserQuota(accountName, -4, 0, 0, UserQuotaState.NORMAL)
|
||||
localUserDataSource.saveQuotaForAccount(accountName, userQuota)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSpacesFromEveryAccountAsStream() =
|
||||
localSpacesDataSource.getSpacesFromEveryAccountAsStream()
|
||||
|
||||
override fun getSpacesByDriveTypeWithSpecialsForAccountAsFlow(accountName: String, filterDriveTypes: Set<String>) =
|
||||
localSpacesDataSource.getSpacesByDriveTypeWithSpecialsForAccountAsFlow(accountName = accountName, filterDriveTypes = filterDriveTypes)
|
||||
|
||||
override fun getPersonalSpaceForAccount(accountName: String) =
|
||||
localSpacesDataSource.getPersonalSpaceForAccount(accountName)
|
||||
|
||||
override fun getPersonalAndProjectSpacesForAccount(accountName: String) =
|
||||
localSpacesDataSource.getPersonalAndProjectSpacesForAccount(accountName)
|
||||
|
||||
override fun getSpaceWithSpecialsByIdForAccount(spaceId: String?, accountName: String) =
|
||||
localSpacesDataSource.getSpaceWithSpecialsByIdForAccount(spaceId, accountName)
|
||||
|
||||
override fun getSpaceByIdForAccount(spaceId: String?, accountName: String): OCSpace? =
|
||||
localSpacesDataSource.getSpaceByIdForAccount(spaceId = spaceId, accountName = accountName)
|
||||
|
||||
override fun getWebDavUrlForSpace(accountName: String, spaceId: String?): String? =
|
||||
localSpacesDataSource.getWebDavUrlForSpace(accountName = accountName, spaceId = spaceId)
|
||||
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.transfers.datasources
|
||||
|
||||
import eu.qsfera.android.domain.transfers.model.OCTransfer
|
||||
import eu.qsfera.android.domain.transfers.model.TransferResult
|
||||
import eu.qsfera.android.domain.transfers.model.TransferStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalTransferDataSource {
|
||||
fun saveTransfer(transfer: OCTransfer): Long
|
||||
fun updateTransfer(transfer: OCTransfer)
|
||||
fun updateTransferStatusToInProgressById(id: Long)
|
||||
fun updateTransferStatusToEnqueuedById(id: Long)
|
||||
fun updateTransferWhenFinished(
|
||||
id: Long,
|
||||
status: TransferStatus,
|
||||
transferEndTimestamp: Long,
|
||||
lastResult: TransferResult
|
||||
)
|
||||
|
||||
fun updateTransferLocalPath(id: Long, localPath: String)
|
||||
fun updateTransferSourcePath(id: Long, sourcePath: String)
|
||||
fun updateTransferStorageDirectoryInLocalPath(
|
||||
id: Long,
|
||||
oldDirectory: String,
|
||||
newDirectory: String
|
||||
)
|
||||
|
||||
fun deleteTransferById(id: Long)
|
||||
fun deleteAllTransfersFromAccount(accountName: String)
|
||||
fun getTransferById(id: Long): OCTransfer?
|
||||
fun getAllTransfers(): List<OCTransfer>
|
||||
fun getAllTransfersAsStream(): Flow<List<OCTransfer>>
|
||||
fun getLastTransferFor(remotePath: String, accountName: String): OCTransfer?
|
||||
fun getCurrentAndPendingTransfers(): List<OCTransfer>
|
||||
fun getFailedTransfers(): List<OCTransfer>
|
||||
fun getFinishedTransfers(): List<OCTransfer>
|
||||
fun clearFailedTransfers()
|
||||
fun clearSuccessfulTransfers()
|
||||
fun existsNonFailedTransferForUri(uri: String): Boolean
|
||||
|
||||
// TUS state management
|
||||
fun updateTusState(
|
||||
id: Long,
|
||||
tusUploadUrl: String?,
|
||||
tusUploadLength: Long?,
|
||||
tusUploadMetadata: String?,
|
||||
tusUploadChecksum: String?,
|
||||
tusResumableVersion: String?,
|
||||
tusUploadExpires: Long?,
|
||||
tusUploadConcat: String?,
|
||||
)
|
||||
|
||||
fun updateTusUrl(id: Long, tusUploadUrl: String?)
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.transfers.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.transfers.datasources.LocalTransferDataSource
|
||||
import eu.qsfera.android.data.transfers.db.OCTransferEntity
|
||||
import eu.qsfera.android.data.transfers.db.TransferDao
|
||||
import eu.qsfera.android.domain.automaticuploads.model.UploadBehavior
|
||||
import eu.qsfera.android.domain.transfers.model.OCTransfer
|
||||
import eu.qsfera.android.domain.transfers.model.TransferResult
|
||||
import eu.qsfera.android.domain.transfers.model.TransferStatus
|
||||
import eu.qsfera.android.domain.transfers.model.UploadEnqueuedBy
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class OCLocalTransferDataSource(
|
||||
private val transferDao: TransferDao
|
||||
) : LocalTransferDataSource {
|
||||
override fun saveTransfer(transfer: OCTransfer): Long =
|
||||
transferDao.insertOrReplace(transfer.toEntity())
|
||||
|
||||
override fun updateTransfer(transfer: OCTransfer) {
|
||||
transferDao.insertOrReplace(transfer.toEntity())
|
||||
}
|
||||
|
||||
override fun updateTransferStatusToInProgressById(id: Long) {
|
||||
transferDao.updateTransferStatusWithId(id, TransferStatus.TRANSFER_IN_PROGRESS.value)
|
||||
}
|
||||
|
||||
override fun updateTransferStatusToEnqueuedById(id: Long) {
|
||||
transferDao.updateTransferStatusWithId(id, TransferStatus.TRANSFER_QUEUED.value)
|
||||
}
|
||||
|
||||
override fun updateTransferWhenFinished(
|
||||
id: Long,
|
||||
status: TransferStatus,
|
||||
transferEndTimestamp: Long,
|
||||
lastResult: TransferResult
|
||||
) {
|
||||
transferDao.updateTransferWhenFinished(id, status.value, transferEndTimestamp, lastResult.value)
|
||||
}
|
||||
|
||||
override fun updateTransferLocalPath(id: Long, localPath: String) {
|
||||
transferDao.updateTransferLocalPath(id, localPath)
|
||||
}
|
||||
|
||||
override fun updateTransferSourcePath(id: Long, sourcePath: String) {
|
||||
transferDao.updateTransferSourcePath(id, sourcePath)
|
||||
}
|
||||
|
||||
override fun updateTransferStorageDirectoryInLocalPath(
|
||||
id: Long,
|
||||
oldDirectory: String,
|
||||
newDirectory: String
|
||||
) {
|
||||
transferDao.updateTransferStorageDirectoryInLocalPath(id, oldDirectory, newDirectory)
|
||||
}
|
||||
|
||||
override fun deleteTransferById(id: Long) {
|
||||
transferDao.deleteTransferWithId(id)
|
||||
}
|
||||
|
||||
override fun deleteAllTransfersFromAccount(accountName: String) {
|
||||
transferDao.deleteTransfersWithAccountName(accountName)
|
||||
}
|
||||
|
||||
override fun getTransferById(id: Long): OCTransfer? =
|
||||
transferDao.getTransferWithId(id)?.toModel()
|
||||
|
||||
override fun getAllTransfers(): List<OCTransfer> =
|
||||
transferDao.getAllTransfers().map { transferEntity ->
|
||||
transferEntity.toModel()
|
||||
}
|
||||
|
||||
override fun getAllTransfersAsStream(): Flow<List<OCTransfer>> =
|
||||
transferDao.getAllTransfersAsStream().map { transferEntitiesList ->
|
||||
val transfers = transferEntitiesList.map { transferEntity ->
|
||||
transferEntity.toModel()
|
||||
}
|
||||
val transfersGroupedByStatus = transfers.groupBy { it.status }
|
||||
val transfersGroupedByStatusOrdered = Array<List<OCTransfer>>(4) { emptyList() }
|
||||
val newTransfersList = mutableListOf<OCTransfer>()
|
||||
transfersGroupedByStatus.forEach { transferMap ->
|
||||
val order = when (transferMap.key) {
|
||||
TransferStatus.TRANSFER_IN_PROGRESS -> 0
|
||||
TransferStatus.TRANSFER_QUEUED -> 1
|
||||
TransferStatus.TRANSFER_FAILED -> 2
|
||||
TransferStatus.TRANSFER_SUCCEEDED -> 3
|
||||
}
|
||||
transfersGroupedByStatusOrdered[order] = transferMap.value
|
||||
}
|
||||
for (items in transfersGroupedByStatusOrdered) {
|
||||
newTransfersList.addAll(items)
|
||||
}
|
||||
newTransfersList
|
||||
}
|
||||
|
||||
override fun getLastTransferFor(remotePath: String, accountName: String): OCTransfer? =
|
||||
transferDao.getLastTransferWithRemotePathAndAccountName(remotePath, accountName)?.toModel()
|
||||
|
||||
override fun getCurrentAndPendingTransfers(): List<OCTransfer> =
|
||||
transferDao.getTransfersWithStatus(
|
||||
listOf(TransferStatus.TRANSFER_IN_PROGRESS.value, TransferStatus.TRANSFER_QUEUED.value)
|
||||
).map { it.toModel() }
|
||||
|
||||
override fun getFailedTransfers(): List<OCTransfer> =
|
||||
transferDao.getTransfersWithStatus(
|
||||
listOf(TransferStatus.TRANSFER_FAILED.value)
|
||||
).map { it.toModel() }
|
||||
|
||||
override fun getFinishedTransfers(): List<OCTransfer> =
|
||||
transferDao.getTransfersWithStatus(
|
||||
listOf(TransferStatus.TRANSFER_SUCCEEDED.value)
|
||||
).map { it.toModel() }
|
||||
|
||||
override fun clearFailedTransfers() {
|
||||
transferDao.deleteTransfersWithStatus(TransferStatus.TRANSFER_FAILED.value)
|
||||
}
|
||||
|
||||
override fun clearSuccessfulTransfers() {
|
||||
transferDao.deleteTransfersWithStatus(TransferStatus.TRANSFER_SUCCEEDED.value)
|
||||
}
|
||||
|
||||
override fun existsNonFailedTransferForUri(uri: String): Boolean =
|
||||
transferDao.existsNonFailedTransferForUri(uri)
|
||||
|
||||
// TUS state management
|
||||
override fun updateTusState(
|
||||
id: Long,
|
||||
tusUploadUrl: String?,
|
||||
tusUploadLength: Long?,
|
||||
tusUploadMetadata: String?,
|
||||
tusUploadChecksum: String?,
|
||||
tusResumableVersion: String?,
|
||||
tusUploadExpires: Long?,
|
||||
tusUploadConcat: String?,
|
||||
) {
|
||||
transferDao.updateTusState(
|
||||
id = id,
|
||||
tusUploadUrl = tusUploadUrl,
|
||||
tusUploadLength = tusUploadLength,
|
||||
tusUploadMetadata = tusUploadMetadata,
|
||||
tusUploadChecksum = tusUploadChecksum,
|
||||
tusResumableVersion = tusResumableVersion,
|
||||
tusUploadExpires = tusUploadExpires,
|
||||
tusUploadConcat = tusUploadConcat,
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateTusUrl(id: Long, tusUploadUrl: String?) {
|
||||
transferDao.updateTusUrl(id = id, tusUploadUrl = tusUploadUrl)
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
@VisibleForTesting
|
||||
fun OCTransferEntity.toModel() = OCTransfer(
|
||||
id = id,
|
||||
localPath = localPath,
|
||||
remotePath = remotePath,
|
||||
accountName = accountName,
|
||||
fileSize = fileSize,
|
||||
status = TransferStatus.fromValue(status),
|
||||
localBehaviour = if (localBehaviour > 1) UploadBehavior.MOVE else UploadBehavior.values()[localBehaviour],
|
||||
forceOverwrite = forceOverwrite,
|
||||
transferEndTimestamp = transferEndTimestamp,
|
||||
lastResult = lastResult?.let { TransferResult.fromValue(it) },
|
||||
createdBy = UploadEnqueuedBy.values()[createdBy],
|
||||
transferId = transferId,
|
||||
spaceId = spaceId,
|
||||
sourcePath = sourcePath,
|
||||
tusUploadUrl = tusUploadUrl,
|
||||
tusUploadLength = tusUploadLength,
|
||||
tusUploadMetadata = tusUploadMetadata,
|
||||
tusUploadChecksum = tusUploadChecksum,
|
||||
tusResumableVersion = tusResumableVersion,
|
||||
tusUploadExpires = tusUploadExpires,
|
||||
tusUploadConcat = tusUploadConcat,
|
||||
)
|
||||
@VisibleForTesting
|
||||
fun OCTransfer.toEntity() = OCTransferEntity(
|
||||
localPath = localPath,
|
||||
remotePath = remotePath,
|
||||
accountName = accountName,
|
||||
fileSize = fileSize,
|
||||
status = status.value,
|
||||
localBehaviour = localBehaviour.ordinal,
|
||||
forceOverwrite = forceOverwrite,
|
||||
transferEndTimestamp = transferEndTimestamp,
|
||||
lastResult = lastResult?.value,
|
||||
createdBy = createdBy.ordinal,
|
||||
transferId = transferId,
|
||||
spaceId = spaceId,
|
||||
sourcePath = sourcePath,
|
||||
tusUploadUrl = tusUploadUrl,
|
||||
tusUploadLength = tusUploadLength,
|
||||
tusUploadMetadata = tusUploadMetadata,
|
||||
tusUploadChecksum = tusUploadChecksum,
|
||||
tusResumableVersion = tusResumableVersion,
|
||||
tusUploadExpires = tusUploadExpires,
|
||||
tusUploadConcat = tusUploadConcat,
|
||||
).apply { this@toEntity.id?.let { this.id = it } }
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.transfers.db
|
||||
|
||||
import android.database.Cursor
|
||||
import android.provider.BaseColumns._ID
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.TRANSFERS_TABLE_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_ACCOUNT_NAME
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_CREATED_BY
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_FILE_SIZE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_FORCE_OVERWRITE
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_LAST_RESULT
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_LOCAL_BEHAVIOUR
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_LOCAL_PATH
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_REMOTE_PATH
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_STATUS
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_TRANSFER_ID
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.UPLOAD_UPLOAD_END_TIMESTAMP
|
||||
import eu.qsfera.android.domain.transfers.model.TransferStatus
|
||||
|
||||
@Entity(
|
||||
tableName = TRANSFERS_TABLE_NAME
|
||||
)
|
||||
data class OCTransferEntity(
|
||||
val localPath: String,
|
||||
val remotePath: String,
|
||||
val accountName: String,
|
||||
val fileSize: Long,
|
||||
val status: Int,
|
||||
val localBehaviour: Int,
|
||||
val forceOverwrite: Boolean,
|
||||
val transferEndTimestamp: Long? = null,
|
||||
val lastResult: Int? = null,
|
||||
val createdBy: Int,
|
||||
val transferId: String? = null,
|
||||
val spaceId: String? = null,
|
||||
val sourcePath: String? = null,
|
||||
// TUS protocol state persistence
|
||||
val tusUploadUrl: String? = null,
|
||||
val tusUploadLength: Long? = null,
|
||||
val tusUploadMetadata: String? = null,
|
||||
val tusUploadChecksum: String? = null,
|
||||
val tusResumableVersion: String? = null,
|
||||
val tusUploadExpires: Long? = null,
|
||||
val tusUploadConcat: String? = null,
|
||||
) {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
companion object {
|
||||
private const val LEGACY_UPLOAD_IN_PROGRESS = 0
|
||||
private const val LEGACY_UPLOAD_FAILED = 1
|
||||
private const val LEGACY_LOCAL_BEHAVIOUR_MOVE = 1
|
||||
private const val LEGACY_LOCAL_BEHAVIOUR_FORGET = 2
|
||||
|
||||
fun fromCursor(cursor: Cursor): OCTransferEntity {
|
||||
val newStatus = when (cursor.getInt(cursor.getColumnIndexOrThrow(UPLOAD_STATUS))) {
|
||||
LEGACY_UPLOAD_IN_PROGRESS -> TransferStatus.TRANSFER_QUEUED.value
|
||||
LEGACY_UPLOAD_FAILED -> TransferStatus.TRANSFER_FAILED.value
|
||||
else -> TransferStatus.TRANSFER_SUCCEEDED.value
|
||||
}
|
||||
val newLocalBehaviour = cursor.getInt(cursor.getColumnIndexOrThrow(UPLOAD_LOCAL_BEHAVIOUR)).let {
|
||||
if (it == LEGACY_LOCAL_BEHAVIOUR_FORGET) LEGACY_LOCAL_BEHAVIOUR_MOVE
|
||||
else it
|
||||
}
|
||||
return OCTransferEntity(
|
||||
localPath = cursor.getString(cursor.getColumnIndexOrThrow(UPLOAD_LOCAL_PATH)),
|
||||
remotePath = cursor.getString(cursor.getColumnIndexOrThrow(UPLOAD_REMOTE_PATH)),
|
||||
accountName = cursor.getString(cursor.getColumnIndexOrThrow(UPLOAD_ACCOUNT_NAME)),
|
||||
fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_FILE_SIZE)),
|
||||
status = newStatus,
|
||||
localBehaviour = newLocalBehaviour,
|
||||
forceOverwrite = cursor.getInt(cursor.getColumnIndexOrThrow(UPLOAD_FORCE_OVERWRITE)) == 1,
|
||||
transferEndTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(UPLOAD_UPLOAD_END_TIMESTAMP)),
|
||||
lastResult = cursor.getInt(cursor.getColumnIndexOrThrow(UPLOAD_LAST_RESULT)),
|
||||
createdBy = cursor.getInt(cursor.getColumnIndexOrThrow(UPLOAD_CREATED_BY)),
|
||||
transferId = cursor.getString(cursor.getColumnIndexOrThrow(UPLOAD_TRANSFER_ID))
|
||||
).apply {
|
||||
id = cursor.getLong(cursor.getColumnIndexOrThrow(_ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.transfers.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.TRANSFERS_TABLE_NAME
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TransferDao {
|
||||
@Query(SELECT_TRANSFER_WITH_ID)
|
||||
fun getTransferWithId(id: Long): OCTransferEntity?
|
||||
|
||||
@Query(SELECT_LAST_TRANSFER_WITH_REMOTE_PATH_AND_ACCOUNT_NAME)
|
||||
fun getLastTransferWithRemotePathAndAccountName(remotePath: String, accountName: String): OCTransferEntity?
|
||||
|
||||
@Query(SELECT_TRANSFERS_WITH_STATUS)
|
||||
fun getTransfersWithStatus(status: List<Int>): List<OCTransferEntity>
|
||||
|
||||
@Query(SELECT_ALL_TRANSFERS)
|
||||
fun getAllTransfers(): List<OCTransferEntity>
|
||||
|
||||
@Query(SELECT_ALL_TRANSFERS)
|
||||
fun getAllTransfersAsStream(): Flow<List<OCTransferEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(ocTransferEntity: OCTransferEntity): Long
|
||||
|
||||
@Query(UPDATE_TRANSFER_STATUS_WITH_ID)
|
||||
fun updateTransferStatusWithId(id: Long, newStatus: Int)
|
||||
|
||||
@Query(UPDATE_TRANSFER_WHEN_FINISHED)
|
||||
fun updateTransferWhenFinished(id: Long, status: Int, transferEndTimestamp: Long, lastResult: Int)
|
||||
|
||||
@Query(UPDATE_TRANSFER_LOCAL_PATH_WITH_ID)
|
||||
fun updateTransferLocalPath(id: Long, localPath: String)
|
||||
|
||||
@Query(UPDATE_TRANSFER_SOURCE_PATH_WITH_ID)
|
||||
fun updateTransferSourcePath(id: Long, sourcePath: String)
|
||||
|
||||
@Query(UPDATE_TRANSFER_STORAGE_DIRECTORY)
|
||||
fun updateTransferStorageDirectoryInLocalPath(id: Long, oldDirectory: String, newDirectory: String)
|
||||
|
||||
// TUS state updates
|
||||
@Query(UPDATE_TUS_STATE)
|
||||
fun updateTusState(
|
||||
id: Long,
|
||||
tusUploadUrl: String?,
|
||||
tusUploadLength: Long?,
|
||||
tusUploadMetadata: String?,
|
||||
tusUploadChecksum: String?,
|
||||
tusResumableVersion: String?,
|
||||
tusUploadExpires: Long?,
|
||||
tusUploadConcat: String?,
|
||||
)
|
||||
|
||||
@Query(UPDATE_TUS_URL)
|
||||
fun updateTusUrl(id: Long, tusUploadUrl: String?)
|
||||
|
||||
@Query(DELETE_TRANSFER_WITH_ID)
|
||||
fun deleteTransferWithId(id: Long)
|
||||
|
||||
@Query(DELETE_TRANSFERS_WITH_ACCOUNT_NAME)
|
||||
fun deleteTransfersWithAccountName(accountName: String)
|
||||
|
||||
@Query(DELETE_TRANSFERS_WITH_STATUS)
|
||||
fun deleteTransfersWithStatus(status: Int)
|
||||
|
||||
@Query(EXISTS_NON_FAILED_TRANSFER_FOR_URI)
|
||||
fun existsNonFailedTransferForUri(uri: String): Boolean
|
||||
|
||||
companion object {
|
||||
private const val SELECT_TRANSFER_WITH_ID = """
|
||||
SELECT *
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val SELECT_LAST_TRANSFER_WITH_REMOTE_PATH_AND_ACCOUNT_NAME = """
|
||||
SELECT *
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE remotePath = :remotePath AND accountName = :accountName
|
||||
ORDER BY transferEndTimestamp DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
private const val SELECT_TRANSFERS_WITH_STATUS = """
|
||||
SELECT *
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE status IN (:status)
|
||||
"""
|
||||
|
||||
private const val SELECT_ALL_TRANSFERS = """
|
||||
SELECT *
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
"""
|
||||
|
||||
private const val UPDATE_TRANSFER_STATUS_WITH_ID = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET status = :newStatus
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val UPDATE_TRANSFER_WHEN_FINISHED = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET status = :status, transferEndTimestamp = :transferEndTimestamp, lastResult = :lastResult
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val UPDATE_TRANSFER_LOCAL_PATH_WITH_ID = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET localPath = :localPath
|
||||
WHERE id = :id
|
||||
"""
|
||||
private const val UPDATE_TRANSFER_SOURCE_PATH_WITH_ID = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET sourcePath = :sourcePath
|
||||
WHERE id = :id
|
||||
"""
|
||||
private const val UPDATE_TRANSFER_STORAGE_DIRECTORY = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET localPath = `REPLACE`(localPath, :oldDirectory, :newDirectory)
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val UPDATE_TUS_STATE = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET tusUploadUrl = :tusUploadUrl,
|
||||
tusUploadLength = :tusUploadLength,
|
||||
tusUploadMetadata = :tusUploadMetadata,
|
||||
tusUploadChecksum = :tusUploadChecksum,
|
||||
tusResumableVersion = :tusResumableVersion,
|
||||
tusUploadExpires = :tusUploadExpires,
|
||||
tusUploadConcat = :tusUploadConcat
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val UPDATE_TUS_URL = """
|
||||
UPDATE $TRANSFERS_TABLE_NAME
|
||||
SET tusUploadUrl = :tusUploadUrl
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val DELETE_TRANSFER_WITH_ID = """
|
||||
DELETE
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE id = :id
|
||||
"""
|
||||
|
||||
private const val DELETE_TRANSFERS_WITH_ACCOUNT_NAME = """
|
||||
DELETE
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE accountName = :accountName
|
||||
"""
|
||||
|
||||
private const val DELETE_TRANSFERS_WITH_STATUS = """
|
||||
DELETE
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE status = :status
|
||||
"""
|
||||
|
||||
/** status 2 = TRANSFER_FAILED, see [TransferStatus] */
|
||||
private const val EXISTS_NON_FAILED_TRANSFER_FOR_URI = """
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM $TRANSFERS_TABLE_NAME
|
||||
WHERE sourcePath = :uri
|
||||
AND status != 2
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Aitor Ballesteros Pavón
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.transfers.repository
|
||||
|
||||
import eu.qsfera.android.data.transfers.datasources.LocalTransferDataSource
|
||||
import eu.qsfera.android.domain.transfers.TransferRepository
|
||||
import eu.qsfera.android.domain.transfers.model.OCTransfer
|
||||
import eu.qsfera.android.domain.transfers.model.TransferResult
|
||||
import eu.qsfera.android.domain.transfers.model.TransferStatus
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class OCTransferRepository(
|
||||
private val localTransferDataSource: LocalTransferDataSource
|
||||
) : TransferRepository {
|
||||
override fun saveTransfer(transfer: OCTransfer) =
|
||||
localTransferDataSource.saveTransfer(transfer = transfer)
|
||||
|
||||
override fun updateTransfer(transfer: OCTransfer) =
|
||||
localTransferDataSource.updateTransfer(transfer = transfer)
|
||||
|
||||
override fun updateTransferStatusToInProgressById(id: Long) {
|
||||
localTransferDataSource.updateTransferStatusToInProgressById(id = id)
|
||||
}
|
||||
|
||||
override fun updateTransferStatusToEnqueuedById(id: Long) {
|
||||
localTransferDataSource.updateTransferStatusToEnqueuedById(id = id)
|
||||
}
|
||||
|
||||
override fun updateTransferLocalPath(id: Long, localPath: String) {
|
||||
localTransferDataSource.updateTransferLocalPath(id = id, localPath = localPath)
|
||||
}
|
||||
|
||||
override fun updateTransferSourcePath(id: Long, sourcePath: String) {
|
||||
localTransferDataSource.updateTransferSourcePath(id = id, sourcePath = sourcePath)
|
||||
}
|
||||
|
||||
override fun updateTransferWhenFinished(
|
||||
id: Long,
|
||||
status: TransferStatus,
|
||||
transferEndTimestamp: Long,
|
||||
lastResult: TransferResult
|
||||
) {
|
||||
localTransferDataSource.updateTransferWhenFinished(
|
||||
id = id,
|
||||
status = status,
|
||||
transferEndTimestamp = transferEndTimestamp,
|
||||
lastResult = lastResult
|
||||
)
|
||||
}
|
||||
|
||||
override fun updateTransferStorageDirectoryInLocalPath(
|
||||
id: Long,
|
||||
oldDirectory: String,
|
||||
newDirectory: String
|
||||
) {
|
||||
localTransferDataSource.updateTransferStorageDirectoryInLocalPath(
|
||||
id = id,
|
||||
oldDirectory = oldDirectory,
|
||||
newDirectory = newDirectory
|
||||
)
|
||||
}
|
||||
|
||||
override fun deleteTransferById(id: Long) =
|
||||
localTransferDataSource.deleteTransferById(id = id)
|
||||
|
||||
override fun deleteAllTransfersFromAccount(accountName: String) =
|
||||
localTransferDataSource.deleteAllTransfersFromAccount(accountName = accountName)
|
||||
|
||||
override fun getTransferById(id: Long): OCTransfer? =
|
||||
localTransferDataSource.getTransferById(id = id)
|
||||
|
||||
override fun getAllTransfers(): List<OCTransfer> =
|
||||
localTransferDataSource.getAllTransfers()
|
||||
|
||||
override fun getAllTransfersAsStream(): Flow<List<OCTransfer>> =
|
||||
localTransferDataSource.getAllTransfersAsStream()
|
||||
|
||||
override fun getLastTransferFor(remotePath: String, accountName: String) =
|
||||
localTransferDataSource.getLastTransferFor(remotePath = remotePath, accountName = accountName)
|
||||
|
||||
override fun getCurrentAndPendingTransfers() =
|
||||
localTransferDataSource.getCurrentAndPendingTransfers()
|
||||
|
||||
override fun getFailedTransfers() =
|
||||
localTransferDataSource.getFailedTransfers()
|
||||
|
||||
override fun getFinishedTransfers() =
|
||||
localTransferDataSource.getFinishedTransfers()
|
||||
|
||||
override fun clearFailedTransfers() =
|
||||
localTransferDataSource.clearFailedTransfers()
|
||||
|
||||
override fun clearSuccessfulTransfers() =
|
||||
localTransferDataSource.clearSuccessfulTransfers()
|
||||
|
||||
override fun existsNonFailedTransferForUri(uri: String): Boolean =
|
||||
localTransferDataSource.existsNonFailedTransferForUri(uri)
|
||||
|
||||
// TUS state management
|
||||
override fun updateTusState(
|
||||
id: Long,
|
||||
tusUploadUrl: String?,
|
||||
tusUploadLength: Long?,
|
||||
tusUploadMetadata: String?,
|
||||
tusUploadChecksum: String?,
|
||||
tusResumableVersion: String?,
|
||||
tusUploadExpires: Long?,
|
||||
tusUploadConcat: String?,
|
||||
) =
|
||||
localTransferDataSource.updateTusState(
|
||||
id = id,
|
||||
tusUploadUrl = tusUploadUrl,
|
||||
tusUploadLength = tusUploadLength,
|
||||
tusUploadMetadata = tusUploadMetadata,
|
||||
tusUploadChecksum = tusUploadChecksum,
|
||||
tusResumableVersion = tusResumableVersion,
|
||||
tusUploadExpires = tusUploadExpires,
|
||||
tusUploadConcat = tusUploadConcat,
|
||||
)
|
||||
|
||||
override fun updateTusUrl(id: Long, tusUploadUrl: String?) =
|
||||
localTransferDataSource.updateTusUrl(id = id, tusUploadUrl = tusUploadUrl)
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Jorge Aguado Recio
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.user.datasources
|
||||
|
||||
import eu.qsfera.android.domain.user.model.UserQuota
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface LocalUserDataSource {
|
||||
fun saveQuotaForAccount(
|
||||
accountName: String,
|
||||
userQuota: UserQuota
|
||||
)
|
||||
|
||||
fun getQuotaForAccount(
|
||||
accountName: String
|
||||
): UserQuota?
|
||||
|
||||
fun getQuotaForAccountAsFlow(
|
||||
accountName: String
|
||||
): Flow<UserQuota?>
|
||||
|
||||
fun getAllUserQuotas(): List<UserQuota>
|
||||
|
||||
fun getAllUserQuotasAsFlow(): Flow<List<UserQuota>>
|
||||
|
||||
fun deleteQuotaForAccount(
|
||||
accountName: String
|
||||
)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* Copyright (C) 2020 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.user.datasources
|
||||
|
||||
import eu.qsfera.android.domain.user.model.UserAvatar
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
import eu.qsfera.android.domain.user.model.UserQuota
|
||||
|
||||
interface RemoteUserDataSource {
|
||||
fun getUserInfo(accountName: String): UserInfo
|
||||
fun getUserQuota(accountName: String): UserQuota
|
||||
fun getUserAvatar(accountName: String): UserAvatar
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Jorge Aguado Recio
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.user.datasources.implementation
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import eu.qsfera.android.data.user.datasources.LocalUserDataSource
|
||||
import eu.qsfera.android.data.user.db.UserDao
|
||||
import eu.qsfera.android.data.user.db.UserQuotaEntity
|
||||
import eu.qsfera.android.domain.user.model.UserQuotaState
|
||||
import eu.qsfera.android.domain.user.model.UserQuota
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class OCLocalUserDataSource(
|
||||
private val userDao: UserDao
|
||||
) : LocalUserDataSource {
|
||||
|
||||
override fun saveQuotaForAccount(accountName: String, userQuota: UserQuota) =
|
||||
userDao.insertOrReplace(userQuota.toEntity())
|
||||
|
||||
override fun getQuotaForAccount(accountName: String): UserQuota? =
|
||||
userDao.getQuotaForAccount(accountName = accountName)?.toModel()
|
||||
|
||||
override fun getQuotaForAccountAsFlow(accountName: String): Flow<UserQuota?> =
|
||||
userDao.getQuotaForAccountAsFlow(accountName = accountName).map { it?.toModel() }
|
||||
|
||||
override fun getAllUserQuotas(): List<UserQuota> =
|
||||
userDao.getAllUserQuotas().map { userQuotaEntity ->
|
||||
userQuotaEntity.toModel()
|
||||
}
|
||||
|
||||
override fun getAllUserQuotasAsFlow(): Flow<List<UserQuota>> =
|
||||
userDao.getAllUserQuotasAsFlow().map { userQuotasList ->
|
||||
userQuotasList.map { it.toModel() }
|
||||
}
|
||||
|
||||
override fun deleteQuotaForAccount(accountName: String) {
|
||||
userDao.deleteQuotaForAccount(accountName = accountName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@VisibleForTesting
|
||||
fun UserQuotaEntity.toModel(): UserQuota =
|
||||
UserQuota(
|
||||
accountName = accountName,
|
||||
available = available,
|
||||
used = used,
|
||||
total = total,
|
||||
state = state?.let { UserQuotaState.fromValue(it) }
|
||||
)
|
||||
|
||||
@VisibleForTesting
|
||||
fun UserQuota.toEntity(): UserQuotaEntity =
|
||||
UserQuotaEntity(
|
||||
accountName = accountName,
|
||||
available = available,
|
||||
used = used,
|
||||
total = total,
|
||||
state = state?.value
|
||||
)
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Jorge Aguado Recio
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.user.datasources.implementation
|
||||
|
||||
import eu.qsfera.android.data.ClientManager
|
||||
import eu.qsfera.android.data.executeRemoteOperation
|
||||
import eu.qsfera.android.data.user.datasources.RemoteUserDataSource
|
||||
import eu.qsfera.android.domain.user.model.UserAvatar
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
import eu.qsfera.android.domain.user.model.UserQuota
|
||||
import eu.qsfera.android.lib.resources.users.GetRemoteUserQuotaOperation
|
||||
import eu.qsfera.android.lib.resources.users.RemoteAvatarData
|
||||
import eu.qsfera.android.lib.resources.users.RemoteUserInfo
|
||||
|
||||
class OCRemoteUserDataSource(
|
||||
private val clientManager: ClientManager,
|
||||
private val avatarDimension: Int
|
||||
) : RemoteUserDataSource {
|
||||
|
||||
override fun getUserInfo(accountName: String): UserInfo =
|
||||
executeRemoteOperation {
|
||||
clientManager.getUserService(accountName).getUserInfo()
|
||||
}.toDomain()
|
||||
|
||||
override fun getUserQuota(accountName: String): UserQuota =
|
||||
executeRemoteOperation {
|
||||
clientManager.getUserService(accountName).getUserQuota()
|
||||
}.toDomain(accountName)
|
||||
|
||||
override fun getUserAvatar(accountName: String): UserAvatar =
|
||||
executeRemoteOperation {
|
||||
clientManager.getUserService(accountName = accountName).getUserAvatar(avatarDimension)
|
||||
}.toDomain()
|
||||
|
||||
}
|
||||
|
||||
/**************************************************************************************************************
|
||||
************************************************* Mappers ****************************************************
|
||||
**************************************************************************************************************/
|
||||
fun RemoteUserInfo.toDomain(): UserInfo =
|
||||
UserInfo(
|
||||
id = this.id,
|
||||
displayName = this.displayName,
|
||||
email = this.email
|
||||
)
|
||||
|
||||
private fun RemoteAvatarData.toDomain(): UserAvatar =
|
||||
UserAvatar(
|
||||
avatarData = this.avatarData,
|
||||
eTag = this.eTag,
|
||||
mimeType = this.mimeType
|
||||
)
|
||||
|
||||
private fun GetRemoteUserQuotaOperation.RemoteQuota.toDomain(accountName: String): UserQuota =
|
||||
UserQuota(
|
||||
accountName = accountName,
|
||||
available = this.free,
|
||||
used = this.used,
|
||||
total = this.total,
|
||||
state = null
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Jorge Aguado Recio
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.user.db
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import eu.qsfera.android.data.ProviderMeta
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface UserDao {
|
||||
@Query(SELECT_QUOTA)
|
||||
fun getQuotaForAccount(
|
||||
accountName: String
|
||||
): UserQuotaEntity?
|
||||
|
||||
@Query(SELECT_QUOTA)
|
||||
fun getQuotaForAccountAsFlow(
|
||||
accountName: String
|
||||
): Flow<UserQuotaEntity?>
|
||||
|
||||
@Query(SELECT_ALL_QUOTAS)
|
||||
fun getAllUserQuotas(): List<UserQuotaEntity>
|
||||
|
||||
@Query(SELECT_ALL_QUOTAS)
|
||||
fun getAllUserQuotasAsFlow(): Flow<List<UserQuotaEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insertOrReplace(userQuotaEntity: UserQuotaEntity)
|
||||
|
||||
@Query(DELETE_QUOTA)
|
||||
fun deleteQuotaForAccount(accountName: String)
|
||||
|
||||
companion object {
|
||||
private const val SELECT_QUOTA = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.USER_QUOTAS_TABLE_NAME}
|
||||
WHERE accountName = :accountName
|
||||
"""
|
||||
|
||||
private const val SELECT_ALL_QUOTAS = """
|
||||
SELECT *
|
||||
FROM ${ProviderMeta.ProviderTableMeta.USER_QUOTAS_TABLE_NAME}
|
||||
"""
|
||||
|
||||
private const val DELETE_QUOTA = """
|
||||
DELETE
|
||||
FROM ${ProviderMeta.ProviderTableMeta.USER_QUOTAS_TABLE_NAME}
|
||||
WHERE accountName = :accountName
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Jorge Aguado Recio
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package eu.qsfera.android.data.user.db
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import eu.qsfera.android.data.ProviderMeta.ProviderTableMeta.USER_QUOTAS_TABLE_NAME
|
||||
|
||||
/**
|
||||
* Represents one record of the UserQuota table.
|
||||
*/
|
||||
@Entity(tableName = USER_QUOTAS_TABLE_NAME)
|
||||
data class UserQuotaEntity(
|
||||
@PrimaryKey
|
||||
val accountName: String,
|
||||
val used: Long,
|
||||
val available: Long,
|
||||
val total: Long? = null,
|
||||
val state: String? = null
|
||||
)
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* qsfera Android client application
|
||||
*
|
||||
* @author Abel García de Prada
|
||||
* @author Juan Carlos Garrote Gascón
|
||||
* @author Jorge Aguado Recio
|
||||
*
|
||||
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package eu.qsfera.android.data.user.repository
|
||||
|
||||
import eu.qsfera.android.data.user.datasources.LocalUserDataSource
|
||||
import eu.qsfera.android.data.user.datasources.RemoteUserDataSource
|
||||
import eu.qsfera.android.domain.user.UserRepository
|
||||
import eu.qsfera.android.domain.user.model.UserAvatar
|
||||
import eu.qsfera.android.domain.user.model.UserInfo
|
||||
import eu.qsfera.android.domain.user.model.UserQuota
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class OCUserRepository(
|
||||
private val localUserDataSource: LocalUserDataSource,
|
||||
private val remoteUserDataSource: RemoteUserDataSource
|
||||
) : UserRepository {
|
||||
override fun getUserInfo(accountName: String): UserInfo = remoteUserDataSource.getUserInfo(accountName)
|
||||
override fun getUserQuota(accountName: String): UserQuota =
|
||||
remoteUserDataSource.getUserQuota(accountName).also {
|
||||
localUserDataSource.saveQuotaForAccount(accountName, it)
|
||||
}
|
||||
|
||||
override fun getStoredUserQuota(accountName: String): UserQuota? =
|
||||
localUserDataSource.getQuotaForAccount(accountName)
|
||||
|
||||
override fun getStoredUserQuotaAsFlow(accountName: String): Flow<UserQuota?> =
|
||||
localUserDataSource.getQuotaForAccountAsFlow(accountName)
|
||||
|
||||
override fun getAllUserQuotas(): List<UserQuota> =
|
||||
localUserDataSource.getAllUserQuotas()
|
||||
|
||||
override fun getAllUserQuotasAsFlow(): Flow<List<UserQuota>> =
|
||||
localUserDataSource.getAllUserQuotasAsFlow()
|
||||
|
||||
override fun getUserAvatar(accountName: String): UserAvatar =
|
||||
remoteUserDataSource.getUserAvatar(accountName)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user