diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b96c1f53..e3e3af3f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,5 @@
default:
- image: ${CI_REGISTRY}/android/shared/docker-android:v1.0.0
+ image: ${CI_REGISTRY}/android/shared/docker-android:v1.0.3
variables:
# Use fastzip to improve cache times
@@ -111,25 +111,25 @@ deploy:review:
build dev debug:
extends: [.build]
needs:
- - job: "detekt analysis"
- job: "prepare-environment"
+ - job: "prepare-build"
script:
- ./gradlew assembleDevDebug
artifacts:
paths:
- ./app/**/*.apk
- rules:
- - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
- - if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_REF_NAME =~ /^test/
- when: manual
- allow_failure: true
build dynamic debug:
extends: [.build]
+ needs:
+ - job: "prepare-environment"
+ - job: "prepare-build"
script:
- export $(cat deploy.env)
- echo HOST="$DYNAMIC_DOMAIN" >> private.properties
- - ./gradlew assembleDynamicDebug assembleDynamicDebugAndroidTest assembleDebugAndroidTest
+ - ./gradlew assembleDynamicDebug --max-workers=4
+ - ./gradlew assembleDynamicDebugAndroidTest --max-workers=4
+ - ./gradlew assembleDebugAndroidTest --max-workers=4
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" || $CI_COMMIT_REF_NAME =~ /^test/
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
@@ -253,6 +253,7 @@ upload to firebase:
--platform android
--job-name $CI_JOB_NAME
--slack-channel drive-android-ci-reports
+ --push-metrics
rules:
# allow failure so non-run tests don't block pipeline
- allow_failure: true
@@ -380,13 +381,11 @@ publish to firebase app distribution:
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
startReview:
- rules:
- - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
- variables:
- PRODUCT_FLAVOR: "dev"
- - if: $CI_PIPELINE_SOURCE == "merge_request_event"
- variables:
- PRODUCT_FLAVOR: "dynamic"
+ needs:
+ - job: "prepare-build"
+ - job: "build dev debug"
+ variables:
+ PRODUCT_FLAVOR: "dev"
before_script:
- if [[ -f /load-env.sh ]]; then source /load-env.sh; fi
- export REVIEW_APP_ARTIFACT_PATH="app/build/outputs/apk/$PRODUCT_FLAVOR/debug/"${ARCHIVES_BASE_NAME}-${PRODUCT_FLAVOR}-debug.apk
diff --git a/.idea/scopes/proton_core.xml b/.idea/scopes/proton_core.xml
index f78e65cf..6b9c9132 100644
--- a/.idea/scopes/proton_core.xml
+++ b/.idea/scopes/proton_core.xml
@@ -1,3 +1,3 @@
-
+
\ No newline at end of file
diff --git a/app-lock/build.gradle.kts b/app-lock/build.gradle.kts
new file mode 100644
index 00000000..5f3ee6cd
--- /dev/null
+++ b/app-lock/build.gradle.kts
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+driveModule(includeSubmodules = true)
diff --git a/app-lock/data/build.gradle.kts b/app-lock/data/build.gradle.kts
new file mode 100644
index 00000000..529fc862
--- /dev/null
+++ b/app-lock/data/build.gradle.kts
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+driveModule(
+ hilt = true,
+ room = true,
+ serialization = true,
+ workManager = true,
+) {
+ api(project(":app-lock:domain"))
+ api(project(":drive:base"))
+ api(libs.androidx.biometric)
+}
diff --git a/app-lock/data/src/main/AndroidManifest.xml b/app-lock/data/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..5acdff14
--- /dev/null
+++ b/app-lock/data/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt
new file mode 100644
index 00000000..886cc50d
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStore.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.crypto
+
+import android.os.Build
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import me.proton.core.drive.base.domain.log.LogTag
+import me.proton.core.util.kotlin.CoreLogger
+import java.security.KeyStore
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.GCMParameterSpec
+
+object Config {
+ const val KEY_STORE_TYPE = "AndroidKeyStore"
+ const val DEFAULT_CIPHER_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
+ const val DEFAULT_CIPHER_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
+ const val DEFAULT_CIPHER_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
+ const val DEFAULT_CIPHER_GCM_TAG_LENGTH = 128
+ const val DEFAULT_CIPHER_IV_BYTES = 12
+ const val DEFAULT_KEY_SIZE = 256
+ const val DEFAULT_USER_AUTHENTICATION_REQUIRED = true
+}
+
+data class SecretKeyProperties(
+ val keyAlias: String,
+ val keyStoreType: String = Config.KEY_STORE_TYPE,
+ val cipherAlgorithm: String = Config.DEFAULT_CIPHER_ALGORITHM,
+ val cipherBlockMode: String = Config.DEFAULT_CIPHER_BLOCK_MODE,
+ val cipherPadding: String = Config.DEFAULT_CIPHER_PADDING,
+ val cipherKeySize: Int = Config.DEFAULT_KEY_SIZE,
+ val userAuthenticationRequired: Boolean = Config.DEFAULT_USER_AUTHENTICATION_REQUIRED,
+)
+
+val SecretKeyProperties.transformation: String get() = "$cipherAlgorithm/$cipherBlockMode/$cipherPadding"
+
+fun SecretKeyProperties.getOrCreateSecretKey(
+ invalidateKeyByBiometricEnrollment: Boolean = true,
+): SecretKey {
+ val keyStore = KeyStore.getInstance(keyStoreType)
+ keyStore.load(null)
+ return if (keyStore.containsAlias(keyAlias)) {
+ keyStore.getKey(keyAlias, null) as SecretKey
+ } else {
+ KeyGenerator.getInstance(
+ cipherAlgorithm,
+ keyStoreType,
+ ).let { keyGenerator ->
+ keyGenerator.init(
+ KeyGenParameterSpec.Builder(
+ keyAlias,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
+ )
+ .setBlockModes(cipherBlockMode)
+ .setEncryptionPaddings(cipherPadding)
+ .setKeySize(cipherKeySize)
+ .defaultKeyGenParameterSpecBuilder(
+ userAuthenticationRequired = userAuthenticationRequired,
+ invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment,
+ )
+ .build()
+ )
+ keyGenerator.generateKey()
+ }
+ }
+}
+
+fun KeyGenParameterSpec.Builder.defaultKeyGenParameterSpecBuilder(
+ userAuthenticationRequired: Boolean,
+ invalidateKeyByBiometricEnrollment: Boolean,
+): KeyGenParameterSpec.Builder {
+ setUserAuthenticationRequired(userAuthenticationRequired)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ setUserAuthenticationParameters(
+ 0,
+ KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL,
+ )
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ setInvalidatedByBiometricEnrollment(invalidateKeyByBiometricEnrollment)
+ }
+ return this
+}
+
+fun SecretKeyProperties.getInitializedCipherForDecryption(
+ initializationVector: ByteArray? = null,
+ invalidateKeyByBiometricEnrollment: Boolean = true,
+ cipherGcmTagLength: Int = Config.DEFAULT_CIPHER_GCM_TAG_LENGTH,
+): Cipher = getCipher(transformation).apply {
+ init(
+ Cipher.DECRYPT_MODE,
+ getOrCreateSecretKey(
+ invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment,
+ ),
+ GCMParameterSpec(cipherGcmTagLength, initializationVector),
+ )
+}
+
+fun SecretKeyProperties.getInitializedCipherForEncryption(
+ invalidateKeyByBiometricEnrollment: Boolean = true,
+): Cipher = getCipher(transformation).apply {
+ init(
+ Cipher.ENCRYPT_MODE,
+ getOrCreateSecretKey(
+ invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment,
+ ),
+ )
+}
+
+private fun getCipher(transformation: String): Cipher = Cipher.getInstance(transformation)
+
+fun removeKey(keyProperties: SecretKeyProperties) {
+ try {
+ KeyStore.getInstance(keyProperties.keyStoreType).run {
+ load(null)
+ deleteEntry(keyProperties.keyAlias)
+ }
+ } catch (e: Exception) {
+ CoreLogger.d(LogTag.DEFAULT, e, e.message.orEmpty())
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt
new file mode 100644
index 00000000..21765617
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/KeyStoreSecretKey.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.crypto
+
+import android.util.Base64
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.core.crypto.common.keystore.EncryptedByteArray
+import me.proton.core.crypto.common.keystore.EncryptedString
+import me.proton.core.crypto.common.keystore.PlainByteArray
+import me.proton.core.crypto.common.keystore.use
+
+class KeyStoreSecretKey(
+ private val keyProperties: SecretKeyProperties,
+ private val invalidateKeyByBiometricEnrollment: Boolean = true,
+) : SecretKey {
+
+ override fun encrypt(value: String): EncryptedString =
+ value.encodeToByteArray().use { plainByteArray ->
+ Base64.encodeToString(
+ encrypt(plainByteArray).array,
+ Base64.NO_WRAP,
+ )
+ }
+
+ override fun encrypt(value: PlainByteArray): EncryptedByteArray {
+ val cipher = keyProperties.getInitializedCipherForEncryption(invalidateKeyByBiometricEnrollment)
+ val cipherByteArray = cipher.doFinal(value.array)
+ return EncryptedByteArray(cipher.iv + cipherByteArray)
+ }
+
+ override fun decrypt(value: EncryptedString): String {
+ val encryptedByteArray = Base64.decode(value, Base64.NO_WRAP)
+ return decrypt(EncryptedByteArray(encryptedByteArray)).use { plainByteArray ->
+ plainByteArray.array.decodeToString()
+ }
+ }
+
+ override fun decrypt(value: EncryptedByteArray): PlainByteArray {
+ val initializationVector = value.array.copyOf(Config.DEFAULT_CIPHER_IV_BYTES)
+ val cipher = keyProperties.getInitializedCipherForDecryption(
+ initializationVector = initializationVector,
+ invalidateKeyByBiometricEnrollment = invalidateKeyByBiometricEnrollment,
+ )
+ val cipherByteArray = value.array.copyOfRange(Config.DEFAULT_CIPHER_IV_BYTES, value.array.size)
+ return PlainByteArray(cipher.doFinal(cipherByteArray))
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt
new file mode 100644
index 00000000..ef95b32a
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/crypto/PgpSecretKey.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.crypto
+
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.core.crypto.common.context.CryptoContext
+import me.proton.core.crypto.common.keystore.EncryptedByteArray
+import me.proton.core.crypto.common.keystore.EncryptedString
+import me.proton.core.crypto.common.keystore.PlainByteArray
+import me.proton.core.crypto.common.pgp.Armored
+import me.proton.core.crypto.common.pgp.Unarmored
+
+class PgpSecretKey(
+ passphrase: PlainByteArray,
+ val lockedKey: Armored,
+ cryptoContext: CryptoContext,
+) : SecretKey {
+ private val pgpCrypto = cryptoContext.pgpCrypto
+ private val unlockedKey: Unarmored
+ private val publicKey: Armored
+
+ init {
+ passphrase.use {
+ unlockedKey = pgpCrypto.unlock(lockedKey, passphrase.array).value
+ publicKey = pgpCrypto.getPublicKey(lockedKey)
+ }
+ }
+
+ override fun encrypt(value: String): EncryptedString = pgpCrypto.encryptText(value, publicKey)
+
+ override fun decrypt(value: EncryptedString): String = pgpCrypto.decryptText(value, unlockedKey)
+
+ override fun encrypt(value: PlainByteArray): EncryptedByteArray =
+ EncryptedByteArray(pgpCrypto.encryptData(value.array, publicKey).toByteArray())
+
+ override fun decrypt(value: EncryptedByteArray): PlainByteArray =
+ PlainByteArray(pgpCrypto.decryptData(pgpCrypto.getArmored(value.array), unlockedKey))
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt
new file mode 100644
index 00000000..cc781c47
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/AppLockDatabase.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.db
+
+import me.proton.android.drive.lock.data.db.dao.AppLockDao
+import me.proton.android.drive.lock.data.db.dao.AutoLockDurationDao
+import me.proton.android.drive.lock.data.db.dao.EnableAppLockDao
+import me.proton.android.drive.lock.data.db.dao.LockDao
+import me.proton.core.data.room.db.Database
+
+interface AppLockDatabase : Database {
+ val appLockDao: AppLockDao
+ val lockDao: LockDao
+ val autoLockDurationDao: AutoLockDurationDao
+ val enableAppLockDao: EnableAppLockDao
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt
new file mode 100644
index 00000000..75ed3918
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AppLockDao.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+import me.proton.android.drive.lock.data.db.entity.AppLockEntity
+import me.proton.core.data.room.db.BaseDao
+
+@Dao
+abstract class AppLockDao : BaseDao() {
+ @Query("""
+ SELECT EXISTS(SELECT * FROM AppLockEntity)
+ """)
+ abstract suspend fun hasAppLock(): Boolean
+
+ @Query("""
+ SELECT EXISTS(SELECT * FROM AppLockEntity)
+ """)
+ abstract fun hasAppLockFlow(): Flow
+
+ @Query("""
+ SELECT * FROM AppLockEntity LIMIT 1
+ """)
+ abstract suspend fun getAppLock(): AppLockEntity
+
+ @Query("""
+ DELETE FROM AppLockEntity
+ """)
+ abstract suspend fun deleteAppLock()
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt
new file mode 100644
index 00000000..f3118a68
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/AutoLockDurationDao.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import kotlinx.coroutines.flow.Flow
+import me.proton.android.drive.lock.data.db.entity.AutoLockDurationEntity
+import me.proton.core.data.room.db.BaseDao
+
+@Dao
+abstract class AutoLockDurationDao : BaseDao() {
+ @Query("""
+ SELECT EXISTS(SELECT * FROM AutoLockDurationEntity WHERE `key` = :key)
+ """)
+ abstract suspend fun hasAutoLockDuration(key: String): Boolean
+
+ @Query("""
+ SELECT * FROM AutoLockDurationEntity WHERE `key` = :key
+ """)
+ abstract fun getAutoLockDuration(key: String): Flow
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt
new file mode 100644
index 00000000..6fcb8ac5
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/EnableAppLockDao.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import me.proton.android.drive.lock.data.db.entity.EnableAppLockEntity
+import me.proton.core.data.room.db.BaseDao
+
+@Dao
+abstract class EnableAppLockDao : BaseDao() {
+ @Query(
+ """
+ SELECT EXISTS(SELECT * FROM EnableAppLockEntity WHERE `key` = :key)
+ """
+ )
+ abstract suspend fun hasEnableAppLock(key: String): Boolean
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt
new file mode 100644
index 00000000..b9512544
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/dao/LockDao.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.db.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import me.proton.android.drive.lock.data.db.entity.LockEntity
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.core.data.room.db.BaseDao
+
+@Dao
+abstract class LockDao : BaseDao() {
+ @Query("""
+ SELECT EXISTS(SELECT * FROM LockEntity WHERE type = :type)
+ """)
+ abstract suspend fun hasLock(type: AppLockType): Boolean
+
+ @Query("""
+ SELECT * FROM LockEntity WHERE type = :type LIMIT 1
+ """)
+ abstract suspend fun getLock(type: AppLockType): LockEntity
+
+ @Query("""
+ DELETE FROM LockEntity WHERE type = :type
+ """)
+ abstract suspend fun deleteLock(type: AppLockType)
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt
new file mode 100644
index 00000000..6db81871
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AppLockEntity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.db.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.core.drive.base.data.db.Column.KEY
+import me.proton.core.drive.base.data.db.Column.TYPE
+
+@Entity(
+ primaryKeys = [KEY],
+)
+data class AppLockEntity(
+ @ColumnInfo(name = KEY)
+ val key: String,
+ @ColumnInfo(name = TYPE)
+ val type: AppLockType,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt
new file mode 100644
index 00000000..a88d6c3b
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/AutoLockDurationEntity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.db.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import me.proton.core.drive.base.data.db.Column.DURATION
+import me.proton.core.drive.base.data.db.Column.KEY
+
+@Entity(
+ primaryKeys = [KEY],
+)
+data class AutoLockDurationEntity(
+ @ColumnInfo(name = KEY)
+ val key: String,
+ @ColumnInfo(name = DURATION)
+ val durationInSeconds: Long,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt
new file mode 100644
index 00000000..05512d10
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/EnableAppLockEntity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.db.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import me.proton.core.drive.base.data.db.Column.KEY
+import me.proton.core.drive.base.data.db.Column.LAST_ACCESS_TIME
+
+@Entity(
+ primaryKeys = [KEY],
+)
+data class EnableAppLockEntity(
+ @ColumnInfo(name = KEY)
+ val key: String,
+ @ColumnInfo(name = LAST_ACCESS_TIME)
+ val timestamp: Long,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt
new file mode 100644
index 00000000..d148e91f
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/db/entity/LockEntity.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.db.entity
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Index
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.core.drive.base.data.db.Column.KEY
+import me.proton.core.drive.base.data.db.Column.PASSPHRASE
+import me.proton.core.drive.base.data.db.Column.TYPE
+
+@Entity(
+ primaryKeys = [PASSPHRASE],
+ foreignKeys = [
+ ForeignKey(
+ entity = AppLockEntity::class,
+ parentColumns = [KEY],
+ childColumns = [KEY],
+ onDelete = ForeignKey.CASCADE
+ )
+ ],
+ indices = [
+ Index(value = [KEY]),
+ ],
+)
+data class LockEntity(
+ @ColumnInfo(name = PASSPHRASE)
+ val appKeyPassphrase: String,
+ @ColumnInfo(name = KEY)
+ val appKey: String,
+ @ColumnInfo(name = TYPE)
+ val type: AppLockType,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt
new file mode 100644
index 00000000..8a89a3d5
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockBindModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import me.proton.android.drive.lock.data.usecase.BuildAppKeyImpl
+import me.proton.android.drive.lock.data.usecase.GeneratePgpSecretKey
+import me.proton.android.drive.lock.data.usecase.GetAppLockImpl
+import me.proton.android.drive.lock.domain.usecase.BuildAppKey
+import me.proton.android.drive.lock.domain.usecase.GenerateSecretKey
+import me.proton.android.drive.lock.domain.usecase.GetAppLock
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+interface AppLockBindModule {
+ @Binds
+ @Singleton
+ fun bindsGetAppLockImpl(impl: GetAppLockImpl): GetAppLock
+
+ @Binds
+ @Singleton
+ fun bindsGeneratePgpSecretKey(impl: GeneratePgpSecretKey): GenerateSecretKey
+
+ @Binds
+ @Singleton
+ fun bindsBuildAppKeyImpl(impl: BuildAppKeyImpl): BuildAppKey
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt
new file mode 100644
index 00000000..443ff579
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/di/AppLockModule.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2021 Proton Technologies AG
+ * This file is part of Proton Technologies AG and ProtonCore.
+ *
+ * ProtonCore is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ProtonCore 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 ProtonCore. If not, see .
+ */
+package me.proton.android.drive.lock.data.di
+
+import android.content.Context
+import android.os.Build
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.work.WorkManager
+import dagger.MapKey
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import dagger.multibindings.IntoMap
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import me.proton.android.drive.lock.data.db.AppLockDatabase
+import me.proton.android.drive.lock.data.lock.CryptoSystemLock
+import me.proton.android.drive.lock.data.lock.SystemLock
+import me.proton.android.drive.lock.data.manager.AppLockManagerImpl
+import me.proton.android.drive.lock.data.manager.AutoLockManagerImpl
+import me.proton.android.drive.lock.data.provider.BiometricPromptProvider
+import me.proton.android.drive.lock.data.provider.BiometricPromptProviderImpl
+import me.proton.android.drive.lock.data.repository.AppLockRepositoryImpl
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.android.drive.lock.domain.manager.AutoLockManager
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration
+import me.proton.android.drive.lock.domain.usecase.LockApp
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppLockModule {
+ @Provides
+ @Singleton
+ fun provideBiometricManager(@ApplicationContext context: Context): BiometricManager =
+ BiometricManager.from(context)
+
+ @Provides
+ @Singleton
+ fun provideBiometricPromptProvider(biometricManager: BiometricManager): BiometricPromptProvider =
+ BiometricPromptProviderImpl(biometricManager)
+
+ @Singleton
+ @Provides
+ fun provideAppLockKeyRepository(
+ appLockDatabase: AppLockDatabase,
+ ): AppLockRepository =
+ AppLockRepositoryImpl(appLockDatabase)
+
+ @Singleton
+ @Provides
+ fun provideAppLockManager(
+ appLockRepository: AppLockRepository,
+ ): AppLockManager =
+ AppLockManagerImpl(appLockRepository, Dispatchers.Main + Job())
+
+ @Singleton
+ @Provides
+ fun provideAutoLockManager(
+ workManager: WorkManager,
+ lockApp: LockApp,
+ getAutoLockDuration: GetAutoLockDuration,
+ ): AutoLockManager =
+ AutoLockManagerImpl(workManager, lockApp, getAutoLockDuration)
+
+ @MapKey
+ annotation class AppLockTypeKey(val value: AppLockType)
+
+ @Singleton
+ @Provides @IntoMap
+ @AppLockTypeKey(AppLockType.SYSTEM)
+ fun provideSystemLock(
+ @ApplicationContext appContext: Context,
+ appLockRepository: AppLockRepository,
+ biometricPromptProvider: BiometricPromptProvider,
+ biometricManager: BiometricManager,
+ ): Lock = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && biometricManager.hasBiometricHardware) {
+ CryptoSystemLock(
+ appContext = appContext,
+ appLockRepository = appLockRepository,
+ biometricPromptProvider = biometricPromptProvider,
+ )
+ } else {
+ SystemLock(
+ appContext = appContext,
+ appLockRepository = appLockRepository,
+ biometricPromptProvider = biometricPromptProvider,
+ )
+ }
+
+ private val BiometricManager.hasBiometricHardware: Boolean get() =
+ canAuthenticate(BIOMETRIC_WEAK) != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt
new file mode 100644
index 00000000..f0d71385
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLock.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.extension
+
+import me.proton.android.drive.lock.data.db.entity.AppLockEntity
+import me.proton.android.drive.lock.domain.entity.AppLock
+
+fun AppLock.toAppLockEntity(): AppLockEntity = AppLockEntity(
+ key = this.key,
+ type = this.type,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt
new file mode 100644
index 00000000..aaf9696a
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/AppLockEntity.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.extension
+
+import me.proton.android.drive.lock.data.db.entity.AppLockEntity
+import me.proton.android.drive.lock.domain.entity.AppLock
+
+fun AppLockEntity.toAppLock() = AppLock(
+ key = this.key,
+ type = this.type,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt
new file mode 100644
index 00000000..094eb3b3
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockEntity.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.extension
+
+import android.util.Base64
+import me.proton.android.drive.lock.data.db.entity.LockEntity
+import me.proton.android.drive.lock.domain.entity.LockKey
+
+fun LockEntity.toLock(): LockKey = LockKey(
+ appKeyPassphrase = Base64.decode(this.appKeyPassphrase, Base64.NO_WRAP),
+ appKey = this.appKey,
+ type = this.type,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt
new file mode 100644
index 00000000..6721681b
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockKey.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.extension
+
+import android.util.Base64
+import me.proton.android.drive.lock.data.db.entity.LockEntity
+import me.proton.android.drive.lock.domain.entity.LockKey
+
+fun LockKey.toLockEntity(): LockEntity = LockEntity(
+ appKeyPassphrase = Base64.encodeToString(this.appKeyPassphrase, Base64.NO_WRAP),
+ appKey = this.appKey,
+ type = this.type,
+)
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt
new file mode 100644
index 00000000..025af919
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/extension/LockState.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.extension
+
+import me.proton.android.drive.lock.domain.lock.LockState
+
+
+fun LockState.onNotAvailable(action: () -> Unit): LockState = this.also {
+ if (this is LockState.NotAvailable) action()
+}
+
+fun LockState.onSetupRequired(action: () -> Unit): LockState = this.also {
+ if (this is LockState.SetupRequired) action()
+}
+
+fun LockState.onReady(action: () -> Unit): LockState = this.also {
+ if (this is LockState.Ready) action()
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt
new file mode 100644
index 00000000..699f685c
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/CryptoSystemLock.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.lock
+
+import android.content.Context
+import androidx.biometric.BiometricPrompt
+import dagger.hilt.android.qualifiers.ApplicationContext
+import me.proton.android.drive.lock.data.crypto.Config
+import me.proton.android.drive.lock.data.crypto.SecretKeyProperties
+import me.proton.android.drive.lock.data.crypto.getInitializedCipherForDecryption
+import me.proton.android.drive.lock.data.crypto.getInitializedCipherForEncryption
+import me.proton.android.drive.lock.data.crypto.removeKey
+import me.proton.android.drive.lock.data.provider.BiometricPromptProvider
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.LockKey
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.lock.LockState
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+import me.proton.core.drive.base.presentation.R as BasePresentation
+
+class CryptoSystemLock @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val appLockRepository: AppLockRepository,
+ private val biometricPromptProvider: BiometricPromptProvider,
+) : Lock {
+ private val keyProperties = SecretKeyProperties(keyAlias = SYSTEM_KEY_ALIAS)
+
+ override suspend fun unlock(
+ key: String,
+ block: suspend (passphrase: ByteArray) -> T,
+ ): Result = coRunCatching {
+ val systemLockKey = appLockRepository.getLockKey(AppLockType.SYSTEM)
+ require(systemLockKey.appKey == key)
+ val initializationVector = systemLockKey.appKeyPassphrase.copyOf(Config.DEFAULT_CIPHER_IV_BYTES)
+ val cipher = biometricPromptProvider.authenticate(
+ title = appContext.getString(BasePresentation.string.app_lock_biometric_title_app_locked),
+ subtitle = appContext.getString(BasePresentation.string.app_lock_biometric_subtitle_app_locked),
+ cryptoObject = BiometricPrompt.CryptoObject(
+ keyProperties.getInitializedCipherForDecryption(
+ initializationVector = initializationVector,
+ )
+ )
+ ).getOrThrow().cryptoObject?.cipher
+ block(
+ requireNotNull(cipher).doFinal(
+ systemLockKey.appKeyPassphrase.copyOfRange(
+ Config.DEFAULT_CIPHER_IV_BYTES,
+ systemLockKey.appKeyPassphrase.size,
+ )
+ )
+ )
+ }
+
+ override suspend fun lock(passphrase: ByteArray): Result = coRunCatching {
+ val cipher = requireNotNull(
+ biometricPromptProvider.authenticate(
+ title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation),
+ subtitle = appContext.getString(
+ BasePresentation.string.app_lock_biometric_subtitle_confirmation_enable
+ ),
+ cryptoObject = BiometricPrompt.CryptoObject(
+ keyProperties.getInitializedCipherForEncryption()
+ )
+ ).getOrThrow().cryptoObject?.cipher
+ )
+ cipher.iv + cipher.doFinal(passphrase)
+ }
+
+ override fun getLockState(): LockState = biometricPromptProvider.getLockState()
+
+ override suspend fun disable(userAuthenticationRequired: Boolean) {
+ if (userAuthenticationRequired) {
+ biometricPromptProvider.authenticate(
+ title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation),
+ subtitle = appContext.getString(
+ BasePresentation.string.app_lock_biometric_subtitle_confirmation_disable
+ ),
+ cryptoObject = null,
+ ).getOrThrow()
+ }
+ appLockRepository.deleteLockKey(AppLockType.SYSTEM)
+ removeKey(keyProperties)
+ }
+
+ override suspend fun enable(lockKey: LockKey) {
+ appLockRepository.insertLockKey(lockKey)
+ }
+
+ companion object {
+ private const val SYSTEM_KEY_ALIAS = "system_lock_key"
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt
new file mode 100644
index 00000000..4371cc1f
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/lock/SystemLock.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.lock
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import me.proton.android.drive.lock.data.crypto.KeyStoreSecretKey
+import me.proton.android.drive.lock.data.crypto.SecretKeyProperties
+import me.proton.android.drive.lock.data.crypto.removeKey
+import me.proton.android.drive.lock.data.provider.BiometricPromptProvider
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.LockKey
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.lock.LockState
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.core.crypto.common.keystore.EncryptedByteArray
+import me.proton.core.crypto.common.keystore.PlainByteArray
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+import me.proton.core.drive.base.presentation.R as BasePresentation
+
+class SystemLock @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val appLockRepository: AppLockRepository,
+ private val biometricPromptProvider: BiometricPromptProvider,
+) : Lock {
+ private val keyProperties = SecretKeyProperties(
+ keyAlias = SYSTEM_KEY_ALIAS,
+ userAuthenticationRequired = false,
+ )
+ private val secretKey: SecretKey = KeyStoreSecretKey(keyProperties)
+
+ override suspend fun unlock(
+ key: String,
+ block: suspend (passphrase: ByteArray) -> T,
+ ): Result = coRunCatching {
+ val lockKey = appLockRepository.getLockKey(AppLockType.SYSTEM)
+ require(lockKey.appKey == key)
+ biometricPromptProvider.authenticate(
+ title = appContext.getString(BasePresentation.string.app_lock_biometric_title_app_locked),
+ subtitle = appContext.getString(BasePresentation.string.app_lock_biometric_subtitle_app_locked),
+ cryptoObject = null,
+ ).getOrThrow()
+ block(secretKey.decrypt(EncryptedByteArray(lockKey.appKeyPassphrase)).array)
+ }
+
+ override suspend fun lock(passphrase: ByteArray): Result = coRunCatching {
+ biometricPromptProvider.authenticate(
+ title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation),
+ subtitle = appContext.getString(BasePresentation.string.app_lock_biometric_subtitle_confirmation_enable),
+ cryptoObject = null,
+ ).getOrThrow()
+ secretKey.encrypt(PlainByteArray(passphrase)).array
+ }
+
+ override suspend fun disable(userAuthenticationRequired: Boolean) {
+ if (userAuthenticationRequired) {
+ biometricPromptProvider.authenticate(
+ title = appContext.getString(BasePresentation.string.app_lock_biometric_title_confirmation),
+ subtitle = appContext.getString(
+ BasePresentation.string.app_lock_biometric_subtitle_confirmation_disable
+ ),
+ cryptoObject = null,
+ ).getOrThrow()
+ }
+ appLockRepository.deleteLockKey(AppLockType.SYSTEM)
+ removeKey(keyProperties)
+ }
+
+ override suspend fun enable(lockKey: LockKey) {
+ appLockRepository.insertLockKey(lockKey)
+ }
+
+ override fun getLockState(): LockState = biometricPromptProvider.getLockState()
+
+ companion object {
+ private const val SYSTEM_KEY_ALIAS = "system_lock_key"
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt
new file mode 100644
index 00000000..4dcc43f1
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AppLockManagerImpl.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.manager
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import me.proton.android.drive.lock.domain.entity.AppLock
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+
+class AppLockManagerImpl @Inject constructor(
+ private val appLockRepository: AppLockRepository,
+ coroutineContext: CoroutineContext,
+) : AppLockManager {
+ private val coroutineScope = CoroutineScope(coroutineContext)
+ override val enabled: StateFlow = appLockRepository.hasAppLockKeyFlow()
+ .stateIn(coroutineScope, SharingStarted.Eagerly, false)
+ private val appKey = MutableStateFlow(null)
+ private val _locked: Flow = appKey.map { secretKey -> secretKey == null }
+ override val locked: StateFlow = combine(enabled, _locked) {
+ _, isLocked ->
+ appLockRepository.hasAppLockKey() && isLocked
+ }.distinctUntilChanged().stateIn(coroutineScope, SharingStarted.Eagerly, false)
+
+ override suspend fun isLocked(): Boolean = isEnabled() && appKey.value == null
+
+ override suspend fun isEnabled(): Boolean = appLockRepository.hasAppLockKey()
+
+ override suspend fun unlock(appKey: SecretKey) {
+ this.appKey.value = appKey
+ }
+
+ override suspend fun lock() {
+ appKey.value = null
+ }
+
+ override suspend fun enable(
+ secretKey: SecretKey,
+ appLock: AppLock,
+ ): Result = coRunCatching {
+ unlock(secretKey)
+ appLockRepository.insertAppLockKey(appLock)
+ appLockRepository.insertOrUpdateEnableAppLockTimestamp(System.currentTimeMillis())
+ true
+ }
+
+ override suspend fun disable(): Result = coRunCatching {
+ appLockRepository.deleteAppLockKey()
+ lock()
+ true
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt
new file mode 100644
index 00000000..96f079a4
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/manager/AutoLockManagerImpl.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.manager
+
+import android.os.Build
+import androidx.work.WorkManager
+import kotlinx.coroutines.flow.first
+import me.proton.android.drive.lock.data.worker.AppLockWorker
+import me.proton.android.drive.lock.domain.manager.AutoLockManager
+import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration
+import me.proton.android.drive.lock.domain.usecase.LockApp
+import javax.inject.Inject
+
+class AutoLockManagerImpl @Inject constructor(
+ private val workManager: WorkManager,
+ private val lockApp: LockApp,
+ private val getAutoLockDuration: GetAutoLockDuration,
+) : AutoLockManager {
+
+ override suspend fun autoLock() {
+ val lockAfter = getAutoLockDuration().first()
+ if (lockAfter.inWholeSeconds == 0L || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ lockApp()
+ } else {
+ workManager.enqueue(
+ AppLockWorker.getWorkRequest(lockAfter, listOf(TAG))
+ )
+ }
+ }
+
+ override fun cancelAutoLock() {
+ workManager.cancelAllWorkByTag(TAG)
+ }
+
+ companion object {
+ private const val TAG = "auto-lock"
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt
new file mode 100644
index 00000000..7cb9f5a7
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProvider.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.provider
+
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.CryptoObject
+import androidx.fragment.app.FragmentActivity
+import me.proton.android.drive.lock.domain.lock.LockState
+
+interface BiometricPromptProvider {
+ fun bindToActivity(activity: FragmentActivity)
+ suspend fun authenticate(
+ title: String,
+ subtitle: String,
+ cryptoObject: CryptoObject?,
+ ): Result
+ fun getLockState(): LockState
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt
new file mode 100644
index 00000000..f4b33514
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/provider/BiometricPromptProviderImpl.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.provider
+
+import android.os.Build
+import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
+import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE
+import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED
+import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
+import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
+import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED
+import androidx.biometric.BiometricManager.BIOMETRIC_STATUS_UNKNOWN
+import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS
+import androidx.biometric.BiometricPrompt
+import androidx.biometric.BiometricPrompt.CryptoObject
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.FragmentActivity
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+import me.proton.android.drive.lock.domain.exception.LockException
+import me.proton.android.drive.lock.domain.lock.LockState
+import me.proton.core.drive.base.domain.util.coRunCatching
+import java.lang.ref.WeakReference
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+
+class BiometricPromptProviderImpl @Inject constructor(
+ private val biometricManager: BiometricManager,
+) : BiometricPromptProvider {
+ private val listeners = ConcurrentHashMap()
+ private var activity: WeakReference = WeakReference(null)
+ private val callback = object : BiometricPrompt.AuthenticationCallback() {
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ listeners.keys().toList().forEach { listener ->
+ listener.onError(LockException.BiometricAuthenticationError(errString.toString()))
+ }
+ }
+
+ override fun onAuthenticationFailed() {
+ super.onAuthenticationFailed()
+ listeners.keys().toList().forEach { listener ->
+ listener.onError(LockException.BiometricAuthenticationFailed)
+ }
+ }
+
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ listeners.keys().toList().forEach { listener -> listener.onSuccess(result) }
+ }
+ }
+
+ interface AuthenticationListener {
+ fun onSuccess(result: BiometricPrompt.AuthenticationResult)
+ fun onError(error: LockException)
+ }
+
+ override fun bindToActivity(activity: FragmentActivity) {
+ this.activity = WeakReference(activity)
+ }
+
+ private fun buildBiometricPrompt(): BiometricPrompt {
+ val activity = this.activity.get()
+ requireNotNull(activity)
+ val executor = ContextCompat.getMainExecutor(activity)
+ return BiometricPrompt(
+ activity,
+ executor,
+ callback,
+ )
+ }
+
+ override suspend fun authenticate(
+ title: String,
+ subtitle: String,
+ cryptoObject: CryptoObject?
+ ): Result = coRunCatching {
+ cryptoObject?.let {
+ buildBiometricPrompt().authenticate(biometricPromptInfo(title, subtitle), cryptoObject)
+ } ?: buildBiometricPrompt().authenticate(biometricPromptInfo(title, subtitle))
+
+ // await result
+ suspendCancellableCoroutine> { continuation ->
+ val listener = object : AuthenticationListener {
+ override fun onSuccess(result: BiometricPrompt.AuthenticationResult) {
+ listeners.remove(this)
+ continuation.resume(Result.success(result))
+ }
+
+ override fun onError(error: LockException) {
+ listeners.remove(this)
+ continuation.resume(Result.failure(error))
+ }
+
+ }
+ listeners[listener] = Unit
+ continuation.invokeOnCancellation {
+ listeners.remove(listener)
+ }
+ }.getOrThrow()
+ }
+
+ override fun getLockState(): LockState =
+ when (val result = biometricManager.canAuthenticate(allowedAuthenticators)) {
+ BIOMETRIC_SUCCESS -> LockState.Ready
+ BIOMETRIC_STATUS_UNKNOWN -> LockState.Ready
+ BIOMETRIC_ERROR_UNSUPPORTED -> LockState.NotAvailable
+ BIOMETRIC_ERROR_HW_UNAVAILABLE -> LockState.Ready
+ BIOMETRIC_ERROR_NONE_ENROLLED -> LockState.SetupRequired
+ BIOMETRIC_ERROR_NO_HARDWARE -> LockState.NotAvailable
+ BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> LockState.NotAvailable
+ else -> error("Unhandled BiometricManager.canAuthenticate result $result")
+ }
+
+ private fun biometricPromptInfo(title: String, subtitle: String): BiometricPrompt.PromptInfo {
+ return BiometricPrompt.PromptInfo.Builder()
+ .setTitle(title)
+ .setSubtitle(subtitle)
+ .setAllowedAuthenticators(allowedAuthenticators)
+ .build()
+ }
+
+ private val allowedAuthenticators: Int get() = when (Build.VERSION.SDK_INT) {
+ Build.VERSION_CODES.P, Build.VERSION_CODES.Q -> BIOMETRIC_WEAK or DEVICE_CREDENTIAL
+ else -> BIOMETRIC_STRONG or DEVICE_CREDENTIAL
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt
new file mode 100644
index 00000000..887ab049
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/repository/AppLockRepositoryImpl.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import me.proton.android.drive.lock.data.db.AppLockDatabase
+import me.proton.android.drive.lock.data.db.entity.AutoLockDurationEntity
+import me.proton.android.drive.lock.data.db.entity.EnableAppLockEntity
+import me.proton.android.drive.lock.data.extension.toAppLock
+import me.proton.android.drive.lock.data.extension.toAppLockEntity
+import me.proton.android.drive.lock.data.extension.toLock
+import me.proton.android.drive.lock.data.extension.toLockEntity
+import me.proton.android.drive.lock.domain.entity.AppLock
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.LockKey
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+
+class AppLockRepositoryImpl @Inject constructor(
+ private val db: AppLockDatabase,
+) : AppLockRepository {
+
+ override fun hasAppLockKeyFlow(): Flow = db.appLockDao.hasAppLockFlow()
+ override suspend fun hasAppLockKey(): Boolean = db.appLockDao.hasAppLock()
+ override suspend fun getAppLockKey(): AppLock = db.appLockDao.getAppLock().toAppLock()
+ override suspend fun insertAppLockKey(appLock: AppLock) = db.appLockDao.insertOrUpdate(appLock.toAppLockEntity())
+ override suspend fun deleteAppLockKey() = db.appLockDao.deleteAppLock()
+
+ override suspend fun hasLockKey(lockType: AppLockType): Boolean = db.lockDao.hasLock(lockType)
+ override suspend fun getLockKey(lockType: AppLockType): LockKey = db.lockDao.getLock(lockType).toLock()
+ override suspend fun insertLockKey(lockKey: LockKey) = db.lockDao.insertOrUpdate(lockKey.toLockEntity())
+ override suspend fun deleteLockKey(lockType: AppLockType) = db.lockDao.deleteLock(lockType)
+
+ override suspend fun hasAutoLockDuration(): Boolean = db.autoLockDurationDao.hasAutoLockDuration(
+ AUTO_LOCK_DURATION_KEY
+ )
+
+ override fun getAutoLockDuration(): Flow = db.autoLockDurationDao.getAutoLockDuration(
+ AUTO_LOCK_DURATION_KEY
+ ).filterNotNull().map { autoLockDurationEntity ->
+ autoLockDurationEntity.durationInSeconds.seconds
+ }
+
+ override suspend fun insertOrUpdateAutoLockDuration(duration: Duration) = db.autoLockDurationDao.insertOrUpdate(
+ AutoLockDurationEntity(
+ key = AUTO_LOCK_DURATION_KEY,
+ durationInSeconds = duration.inWholeSeconds,
+ )
+ )
+
+ override suspend fun hasEnableAppLockTimestamp(): Boolean = db.enableAppLockDao.hasEnableAppLock(ENABLE_LOCK_KEY)
+
+ override suspend fun insertOrUpdateEnableAppLockTimestamp(timestamp: Long) = db.enableAppLockDao.insertOrUpdate(
+ EnableAppLockEntity(
+ key = ENABLE_LOCK_KEY,
+ timestamp = timestamp,
+ )
+ )
+
+ companion object {
+ private const val AUTO_LOCK_DURATION_KEY = "auto-lock"
+ private const val ENABLE_LOCK_KEY = "enable-lock"
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt
new file mode 100644
index 00000000..905e65f9
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/BuildAppKeyImpl.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.usecase
+
+import me.proton.android.drive.lock.data.crypto.PgpSecretKey
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.usecase.BuildAppKey
+import me.proton.core.crypto.common.context.CryptoContext
+import me.proton.core.crypto.common.keystore.PlainByteArray
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+
+class BuildAppKeyImpl @Inject constructor(
+ private val cryptoContext: CryptoContext,
+) : BuildAppKey {
+ override suspend operator fun invoke(key: String, lock: Lock): Result = coRunCatching {
+ lock.unlock(key) { passphrase ->
+ PgpSecretKey(
+ passphrase = PlainByteArray(passphrase),
+ lockedKey = key,
+ cryptoContext = cryptoContext,
+ )
+ }.getOrThrow()
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt
new file mode 100644
index 00000000..ebbd82a9
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GeneratePgpSecretKey.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.usecase
+
+import me.proton.android.drive.lock.data.crypto.PgpSecretKey
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.android.drive.lock.domain.usecase.GenerateSecretKey
+import me.proton.core.crypto.common.context.CryptoContext
+import me.proton.core.crypto.common.keystore.PlainByteArray
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+
+class GeneratePgpSecretKey @Inject constructor(
+ private val cryptoContext: CryptoContext,
+) : GenerateSecretKey {
+
+ override suspend fun invoke(
+ passphrase: ByteArray,
+ ): Result = coRunCatching {
+ cryptoContext.pgpCrypto.generateRandomBytes()
+ PgpSecretKey(
+ passphrase = PlainByteArray(passphrase.clone()),
+ lockedKey = cryptoContext.pgpCrypto.generateNewPrivateKey(DEFAULT_USERNAME, DEFAULT_DOMAIN, passphrase),
+ cryptoContext = cryptoContext,
+ )
+ }
+
+ companion object {
+ private const val DEFAULT_USERNAME = "drive-app-lock-key"
+ private const val DEFAULT_DOMAIN = "proton.me"
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt
new file mode 100644
index 00000000..fdaf52ee
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/usecase/GetAppLockImpl.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.data.usecase
+
+import me.proton.android.drive.lock.data.crypto.PgpSecretKey
+import me.proton.android.drive.lock.domain.entity.AppLock
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.android.drive.lock.domain.usecase.GetAppLock
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+
+class GetAppLockImpl @Inject constructor() : GetAppLock {
+ override operator fun invoke(secretKey: SecretKey, appLockType: AppLockType): Result = coRunCatching {
+ require(secretKey is PgpSecretKey)
+ AppLock(
+ key = secretKey.lockedKey,
+ type = appLockType,
+ )
+ }
+}
diff --git a/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt
new file mode 100644
index 00000000..4768f64d
--- /dev/null
+++ b/app-lock/data/src/main/kotlin/me/proton/android/drive/lock/data/worker/AppLockWorker.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.data.worker
+
+import android.annotation.TargetApi
+import android.content.Context
+import android.os.Build
+import androidx.hilt.work.HiltWorker
+import androidx.work.CoroutineWorker
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkerParameters
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedInject
+import me.proton.android.drive.lock.domain.usecase.LockApp
+import me.proton.core.drive.base.data.workmanager.addTags
+import java.util.concurrent.TimeUnit
+import kotlin.time.Duration
+
+@HiltWorker
+@TargetApi(Build.VERSION_CODES.O)
+class AppLockWorker @AssistedInject constructor(
+ @Assisted appContext: Context,
+ @Assisted workerParams: WorkerParameters,
+ private val lockApp: LockApp,
+) : CoroutineWorker(appContext, workerParams) {
+
+ override suspend fun doWork(): Result {
+ lockApp()
+ return Result.success()
+ }
+
+ companion object {
+ fun getWorkRequest(
+ runAfter: Duration,
+ tags: List = emptyList(),
+ ): OneTimeWorkRequest =
+ OneTimeWorkRequest.Builder(AppLockWorker::class.java)
+ .setInitialDelay(runAfter.inWholeSeconds, TimeUnit.SECONDS)
+ .addTags(tags)
+ .build()
+ }
+}
diff --git a/app-lock/domain/build.gradle.kts b/app-lock/domain/build.gradle.kts
new file mode 100644
index 00000000..07d41c2e
--- /dev/null
+++ b/app-lock/domain/build.gradle.kts
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+driveModule(
+ hilt = true,
+ serialization = true,
+) {
+ api(project(":drive:link:domain"))
+ api(project(":drive:crypto-base:domain"))
+}
diff --git a/app-lock/domain/src/main/AndroidManifest.xml b/app-lock/domain/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..4a0a4237
--- /dev/null
+++ b/app-lock/domain/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt
new file mode 100644
index 00000000..927ed0b4
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLock.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.entity
+
+data class AppLock(
+ val key: String,
+ val type: AppLockType,
+)
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt
new file mode 100644
index 00000000..c80f72fa
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/AppLockType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.entity
+
+enum class AppLockType {
+ SYSTEM,
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt
new file mode 100644
index 00000000..2d2b77a1
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/LockKey.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.entity
+
+data class LockKey(
+ val appKeyPassphrase: ByteArray,
+ val appKey: String,
+ val type: AppLockType,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as LockKey
+
+ if (!appKeyPassphrase.contentEquals(other.appKeyPassphrase)) return false
+ if (appKey != other.appKey) return false
+ if (type != other.type) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = appKeyPassphrase.contentHashCode()
+ result = 31 * result + appKey.hashCode()
+ result = 31 * result + type.hashCode()
+ return result
+ }
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt
new file mode 100644
index 00000000..4e64ad34
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/entity/SecretKey.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.entity
+
+import me.proton.core.crypto.common.keystore.EncryptedByteArray
+import me.proton.core.crypto.common.keystore.EncryptedString
+import me.proton.core.crypto.common.keystore.PlainByteArray
+
+interface SecretKey {
+ /**
+ * Encrypt a [String] [value] and return an [EncryptedString].
+ */
+ fun encrypt(value: String): EncryptedString
+
+ /**
+ * Decrypt an [EncryptedString] [value] and return a [String].
+ */
+ fun decrypt(value: EncryptedString): String
+
+ /**
+ * Encrypt a [PlainByteArray] [value] and return an [EncryptedByteArray].
+ */
+ fun encrypt(value: PlainByteArray): EncryptedByteArray
+
+ /**
+ * Decrypt an [EncryptedByteArray] [value] and return a [PlainByteArray].
+ */
+ fun decrypt(value: EncryptedByteArray): PlainByteArray
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt
new file mode 100644
index 00000000..19066bf3
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/exception/LockException.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.exception
+
+import me.proton.core.drive.base.domain.exception.DriveException
+
+sealed class LockException : DriveException() {
+ object BiometricAuthenticationFailed : LockException()
+ data class BiometricAuthenticationError(val errorMessage: String) : LockException()
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt
new file mode 100644
index 00000000..9ca9a8c4
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/lock/Lock.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.lock
+
+import me.proton.android.drive.lock.domain.entity.LockKey
+
+interface Lock {
+ /**
+ * Unlocks passphrase for a given [key] and provides it to the [block].
+ * After [block] is done, passphrase should not be available anymore.
+ */
+ suspend fun unlock(key: String, block: suspend (passphrase: ByteArray) -> T): Result
+
+ /**
+ * Locks [passphrase] so that it's safe to store.
+ */
+ suspend fun lock(passphrase: ByteArray): Result
+
+ /**
+ * Called when [Lock] should not be used anymore. If [userAuthenticationRequired] is true then user authentication
+ * is required before disabling can be done.
+ */
+ suspend fun disable(userAuthenticationRequired: Boolean)
+
+ /**
+ * Called when [Lock] should protect [lockKey]
+ */
+ suspend fun enable(lockKey: LockKey)
+
+ /**
+ * Provides current [Lock] state. See [LockState].
+ */
+ fun getLockState(): LockState
+}
+
+sealed interface LockState {
+ object NotAvailable : LockState
+ object SetupRequired : LockState
+ object Ready : LockState
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt
new file mode 100644
index 00000000..c7ff6b97
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AppLockManager.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.manager
+
+import kotlinx.coroutines.flow.StateFlow
+import me.proton.android.drive.lock.domain.entity.AppLock
+import me.proton.android.drive.lock.domain.entity.SecretKey
+
+interface AppLockManager {
+ val locked: StateFlow
+ val enabled: StateFlow
+ suspend fun isLocked(): Boolean
+ suspend fun isEnabled(): Boolean
+ suspend fun unlock(appKey: SecretKey)
+ suspend fun lock()
+ suspend fun enable(secretKey: SecretKey, appLock: AppLock): Result
+ suspend fun disable(): Result
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt
new file mode 100644
index 00000000..fd26a9ea
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/manager/AutoLockManager.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.manager
+
+interface AutoLockManager {
+ suspend fun autoLock()
+ fun cancelAutoLock()
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt
new file mode 100644
index 00000000..7bf6ba5a
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/repository/AppLockRepository.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.repository
+
+import kotlinx.coroutines.flow.Flow
+import me.proton.android.drive.lock.domain.entity.AppLock
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.LockKey
+import kotlin.time.Duration
+
+interface AppLockRepository {
+ fun hasAppLockKeyFlow(): Flow
+ suspend fun hasAppLockKey(): Boolean
+ suspend fun getAppLockKey(): AppLock
+ suspend fun insertAppLockKey(appLock: AppLock)
+ suspend fun deleteAppLockKey()
+
+ suspend fun hasLockKey(lockType: AppLockType): Boolean
+ suspend fun getLockKey(lockType: AppLockType): LockKey
+ suspend fun insertLockKey(lockKey: LockKey)
+ suspend fun deleteLockKey(lockType: AppLockType)
+
+ suspend fun hasAutoLockDuration(): Boolean
+ fun getAutoLockDuration(): Flow
+ suspend fun insertOrUpdateAutoLockDuration(duration: Duration)
+
+ suspend fun hasEnableAppLockTimestamp(): Boolean
+ suspend fun insertOrUpdateEnableAppLockTimestamp(timestamp: Long)
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt
new file mode 100644
index 00000000..558b66a0
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/BuildAppKey.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.SecretKey
+import me.proton.android.drive.lock.domain.lock.Lock
+
+interface BuildAppKey {
+ suspend operator fun invoke(key: String, lock: Lock): Result
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt
new file mode 100644
index 00000000..ad61ae86
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/DisableAppLock.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.core.drive.base.domain.util.coRunCatching
+import me.proton.core.util.kotlin.CoreLogger
+import javax.inject.Inject
+
+class DisableAppLock @Inject constructor(
+ private val appLockManager: AppLockManager,
+ private val locks: @JvmSuppressWildcards Map,
+) {
+ suspend operator fun invoke(
+ lockType: AppLockType = AppLockType.SYSTEM,
+ userAuthenticationRequired: Boolean = true,
+ ): Result = coRunCatching {
+ if (appLockManager.isEnabled()) {
+ requireNotNull(locks[lockType]).disable(userAuthenticationRequired)
+ appLockManager.disable().getOrThrow()
+ }
+ }
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt
new file mode 100644
index 00000000..4fe0a1af
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/EnableAppLock.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.LockKey
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.core.drive.base.domain.util.coRunCatching
+import me.proton.core.drive.cryptobase.domain.usecase.GeneratePassphrase
+import javax.inject.Inject
+
+class EnableAppLock @Inject constructor(
+ private val generatePassphrase: GeneratePassphrase,
+ private val generateSecretKey: GenerateSecretKey,
+ private val getAppLock: GetAppLock,
+ private val appLockManager: AppLockManager,
+ private val locks: @JvmSuppressWildcards Map,
+) {
+
+ suspend operator fun invoke(lockType: AppLockType = AppLockType.SYSTEM) = coRunCatching {
+ val passphrase = generatePassphrase()
+ val secretKey = generateSecretKey(passphrase).getOrThrow()
+ val appLock = getAppLock(secretKey, lockType).getOrThrow()
+ val lock = requireNotNull(locks[lockType])
+ val lockKey = LockKey(
+ appKeyPassphrase = lock.lock(passphrase).getOrThrow(),
+ appKey = appLock.key,
+ type = appLock.type,
+ )
+ appLockManager.enable(secretKey, appLock).getOrThrow()
+ lock.enable(lockKey)
+ }
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt
new file mode 100644
index 00000000..7764419a
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GenerateSecretKey.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.SecretKey
+
+interface GenerateSecretKey {
+ suspend operator fun invoke(passphrase: ByteArray): Result
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt
new file mode 100644
index 00000000..ce57ae45
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAppLock.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.AppLock
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.entity.SecretKey
+
+interface GetAppLock {
+ operator fun invoke(secretKey: SecretKey, appLockType: AppLockType): Result
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt
new file mode 100644
index 00000000..55bb8617
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetAutoLockDuration.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flow
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import javax.inject.Inject
+import kotlin.time.Duration
+
+class GetAutoLockDuration @Inject constructor(
+ private val appLockRepository: AppLockRepository,
+ private val configurationProvider: ConfigurationProvider,
+) {
+ operator fun invoke(): Flow = flow {
+ if (appLockRepository.hasAutoLockDuration().not()) {
+ emit(configurationProvider.autoLockDurations.first())
+ }
+ emitAll(appLockRepository.getAutoLockDuration())
+ }
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt
new file mode 100644
index 00000000..6644041b
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/GetLockState.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.lock.LockState
+import javax.inject.Inject
+
+class GetLockState @Inject constructor(
+ private val locks: @JvmSuppressWildcards Map,
+) {
+ operator fun invoke(appLockType: AppLockType = AppLockType.SYSTEM): LockState =
+ requireNotNull(locks[appLockType]).getLockState()
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt
new file mode 100644
index 00000000..690fa471
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/HasEnableAppLockTimestamp.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import javax.inject.Inject
+
+class HasEnableAppLockTimestamp @Inject constructor(
+ private val appLockRepository: AppLockRepository,
+) {
+ suspend operator fun invoke(): Boolean =
+ appLockRepository.hasEnableAppLockTimestamp()
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt
new file mode 100644
index 00000000..642f8da9
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/LockApp.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import javax.inject.Inject
+
+class LockApp @Inject constructor(
+ private val appLockManager: AppLockManager,
+) {
+ suspend operator fun invoke() {
+ if (appLockManager.isEnabled() && appLockManager.isLocked().not()) {
+ appLockManager.lock()
+ }
+ }
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt
new file mode 100644
index 00000000..d440cb54
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UnlockApp.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.lock.Lock
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+
+class UnlockApp @Inject constructor(
+ private val appLockManager: AppLockManager,
+ private val appLockRepository: AppLockRepository,
+ private val locks: @JvmSuppressWildcards Map,
+ private val buildAppKey: BuildAppKey,
+) {
+ suspend operator fun invoke(): Result = coRunCatching {
+ if (appLockManager.isEnabled() && appLockManager.isLocked()) {
+ val appLockKey = appLockRepository.getAppLockKey()
+ appLockManager.unlock(
+ buildAppKey(appLockKey.key, requireNotNull(locks[appLockKey.type])).getOrThrow()
+ )
+ }
+ }
+}
diff --git a/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt
new file mode 100644
index 00000000..eab69a4e
--- /dev/null
+++ b/app-lock/domain/src/main/kotlin/me/proton/android/drive/lock/domain/usecase/UpdateAutoLockDuration.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.domain.usecase
+
+import me.proton.android.drive.lock.domain.repository.AppLockRepository
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.base.domain.util.coRunCatching
+import javax.inject.Inject
+import kotlin.time.Duration
+
+class UpdateAutoLockDuration @Inject constructor(
+ private val appLockRepository: AppLockRepository,
+ private val configurationProvider: ConfigurationProvider,
+) {
+ suspend operator fun invoke(duration: Duration): Result = coRunCatching {
+ require(configurationProvider.autoLockDurations.contains(duration)) {
+ "Only values from ConfigurationProvider#autoLockDurations are allowed"
+ }
+ appLockRepository.insertOrUpdateAutoLockDuration(duration)
+ }
+}
diff --git a/app-lock/presentation/build.gradle.kts b/app-lock/presentation/build.gradle.kts
new file mode 100644
index 00000000..559b9a0d
--- /dev/null
+++ b/app-lock/presentation/build.gradle.kts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+driveModule(
+ hilt = true,
+ compose = true,
+) {
+ api(project(":app-lock:domain"))
+ implementation(project(":drive:base:presentation"))
+ implementation(libs.accompanist.drawablepainter)
+}
diff --git a/app-lock/presentation/src/main/AndroidManifest.xml b/app-lock/presentation/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..80f095b1
--- /dev/null
+++ b/app-lock/presentation/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt
new file mode 100644
index 00000000..a662cac0
--- /dev/null
+++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/AppLock.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.presentation.component
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import me.proton.core.account.domain.entity.Account
+import me.proton.core.domain.entity.UserId
+
+@Composable
+fun AppLock(
+ locked: Flow,
+ primaryAccount: Flow,
+ content: @Composable () -> Unit,
+) {
+ var isLocked by remember { mutableStateOf(false) }
+ var userId by remember { mutableStateOf(null) }
+ LaunchedEffect(Unit) {
+ locked
+ .onEach { locked ->
+ isLocked = locked
+ }
+ .launchIn(this)
+ primaryAccount
+ .onEach { account ->
+ userId = account?.userId
+ }
+ .launchIn(this)
+ }
+ Crossfade(targetState = isLocked) { appLocked ->
+ if (appLocked) {
+ Unlock(
+ userId = userId,
+ modifier = Modifier,
+ )
+ } else {
+ content()
+ }
+ }
+}
diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt
new file mode 100644
index 00000000..73cac280
--- /dev/null
+++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/component/Unlock.kt
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.presentation.component
+
+import android.graphics.drawable.Drawable
+import androidx.annotation.DrawableRes
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.widthIn
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import me.proton.android.drive.lock.presentation.R
+import me.proton.android.drive.lock.presentation.viewevent.UnlockViewEvent
+import me.proton.android.drive.lock.presentation.viewmodel.UnlockViewModel
+import me.proton.core.compose.component.ProtonSolidButton
+import me.proton.core.compose.component.ProtonTextButton
+import me.proton.core.compose.theme.ProtonDimens.LargeSpacing
+import me.proton.core.compose.theme.ProtonDimens.ListItemHeight
+import me.proton.core.compose.theme.ProtonDimens.SmallSpacing
+import me.proton.core.compose.theme.ProtonTheme
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.presentation.extension.conditional
+import me.proton.core.drive.base.presentation.extension.isLandscape
+import me.proton.core.drive.base.presentation.extension.isPortrait
+import me.proton.core.drive.base.presentation.extension.shadow
+import me.proton.core.drive.base.presentation.R as BasePresentation
+import me.proton.core.presentation.R as CorePresentation
+
+@Composable
+fun Unlock(
+ userId: UserId?,
+ modifier: Modifier = Modifier,
+) {
+ val viewModel = hiltViewModel()
+ Unlock(
+ userId = userId,
+ viewEvent = viewModel.viewEvent,
+ modifier = modifier,
+ )
+}
+
+@Composable
+fun Unlock(
+ userId: UserId?,
+ viewEvent: UnlockViewEvent,
+ modifier: Modifier = Modifier,
+) {
+ LaunchedEffect(Unit) {
+ viewEvent.onShowBiometric()
+ }
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .conditional(isPortrait) {
+ navigationBarsPadding()
+ }
+ ) {
+ LogoHeader(
+ modifier = Modifier
+ .weight(LogoHeaderWeight)
+ )
+ Actions(
+ modifier = Modifier
+ .weight(1f - LogoHeaderWeight),
+ onUnlock = { viewEvent.onShowBiometric() },
+ onSignOut = { viewEvent.onSignOut(userId) },
+ )
+ }
+
+}
+
+@Composable
+private fun LogoHeader(
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ ) {
+ Image(
+ painter = rememberDrawablePainter(
+ drawable = getDrawable(
+ light = R.drawable.welcome_header_light,
+ dark = R.drawable.welcome_header_dark,
+ dayNight = R.drawable.welcome_header,
+ )
+ ),
+ contentDescription = null,
+ contentScale = ContentScale.None,
+ )
+ val y = LogoTranslationY
+ Image(
+ painter = painterResource(id = CorePresentation.drawable.ic_logo_drive),
+ contentDescription = null,
+ modifier = Modifier
+ .size(LogoSize)
+ .align(Alignment.BottomCenter)
+ .graphicsLayer {
+ translationX = 0f
+ translationY = y
+ }
+ .shadow(
+ color = ShadowColor,
+ alpha = DriveLogoShadowAlpha,
+ cornersRadius = DriveLogoShadowCornerRadius,
+ blurRadius = DriveLogoShadowBlurRadius,
+ offsetY = DriveLogoShadowOffsetY,
+ )
+ )
+ }
+}
+
+@Composable
+private fun Actions(
+ modifier: Modifier = Modifier,
+ onUnlock: () -> Unit,
+ onSignOut: () -> Unit,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth(),
+ ) {
+ Image(
+ painter = rememberDrawablePainter(
+ drawable = getDrawable(
+ light = CorePresentation.drawable.logo_drive_dark,
+ dark = CorePresentation.drawable.logo_drive_light,
+ dayNight = CorePresentation.drawable.logo_drive_daylight,
+ )
+ ),
+ contentDescription = null,
+ modifier = Modifier
+ .padding(top = DriveLogoTopPadding)
+ .heightIn(max = DriveLogoHeight)
+ .align(Alignment.TopCenter)
+ )
+ val buttonModifier = Modifier
+ .conditional(isPortrait) {
+ fillMaxWidth()
+ }
+ .conditional(isLandscape) {
+ widthIn(min = ButtonMinWidth)
+ }
+ .heightIn(min = ListItemHeight)
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(all = LargeSpacing)
+ .align(Alignment.BottomCenter)
+ ) {
+ ProtonSolidButton(
+ onClick = onUnlock,
+ modifier = buttonModifier,
+ ) {
+ Text(text = stringResource(id = BasePresentation.string.app_lock_unlock_the_app))
+ }
+ Spacer(
+ modifier = Modifier
+ .fillMaxWidth()
+ .heightIn(min = SmallSpacing)
+ )
+ ProtonTextButton(
+ onClick = onSignOut,
+ modifier = buttonModifier,
+ ) {
+ Text(text = stringResource(id = BasePresentation.string.title_sign_out))
+ }
+ }
+ }
+}
+
+@Composable
+private fun getDrawable(@DrawableRes light: Int, @DrawableRes dark: Int, @DrawableRes dayNight: Int): Drawable? =
+ AppCompatResources.getDrawable(
+ LocalContext.current,
+ when (AppCompatDelegate.getDefaultNightMode()) {
+ AppCompatDelegate.MODE_NIGHT_YES -> dark
+ AppCompatDelegate.MODE_NIGHT_NO -> light
+ else -> dayNight
+ }
+ )
+
+@Preview
+@Composable
+private fun UnlockPreview() {
+ ProtonTheme {
+ Unlock(
+ userId = null,
+ viewEvent = object : UnlockViewEvent {
+ override val onShowBiometric = {}
+ override val onSignOut: (UserId?) -> Unit = {}
+ }
+ )
+ }
+}
+
+private val ShadowColor = Color(0xFF0D052E)
+private val LogoSize = 106.dp
+private val LogoTranslationY: Float @Composable get() = if (isPortrait) {
+ LocalDensity.current.run { 44.dp.toPx() }
+} else {
+ LocalDensity.current.run { 42.dp.toPx() }
+}
+private val LogoHeaderWeight: Float @Composable get() = if (isPortrait) 0.3f else 0.25f
+private val ButtonMinWidth = 300.dp
+private val DriveLogoHeight = 32.dp
+private val DriveLogoTopPadding = 60.dp
+private const val DriveLogoShadowAlpha = 0.07f
+private val DriveLogoShadowCornerRadius = 24.dp
+private val DriveLogoShadowBlurRadius = 16.dp
+private val DriveLogoShadowOffsetY = 8.dp
diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt
new file mode 100644
index 00000000..79fc8a0d
--- /dev/null
+++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/extension/LockException.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.lock.presentation.extension
+
+import android.content.Context
+import me.proton.android.drive.lock.domain.exception.LockException
+import me.proton.core.drive.base.presentation.R as BasePresentation
+
+fun LockException.getDefaultMessage(context: Context): String = when (this) {
+ is LockException.BiometricAuthenticationFailed -> context.getString(
+ BasePresentation.string.app_lock_system_biometrics_authentication_failed
+ )
+ is LockException.BiometricAuthenticationError -> errorMessage
+ else -> error("Default message for exception is missing")
+}
diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt
new file mode 100644
index 00000000..a106cb27
--- /dev/null
+++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewevent/UnlockViewEvent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.presentation.viewevent
+
+import me.proton.core.domain.entity.UserId
+
+interface UnlockViewEvent {
+ val onShowBiometric: () -> Unit
+ val onSignOut: (UserId?) -> Unit
+}
diff --git a/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt
new file mode 100644
index 00000000..c1a85e23
--- /dev/null
+++ b/app-lock/presentation/src/main/kotlin/me/proton/android/drive/lock/presentation/viewmodel/UnlockViewModel.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.lock.presentation.viewmodel
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import me.proton.android.drive.lock.domain.exception.LockException
+import me.proton.android.drive.lock.domain.usecase.UnlockApp
+import me.proton.android.drive.lock.presentation.viewevent.UnlockViewEvent
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.domain.usecase.SignOut
+import me.proton.android.drive.lock.presentation.extension.getDefaultMessage
+import me.proton.core.accountmanager.domain.AccountManager
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.base.domain.usecase.BroadcastMessages
+import me.proton.core.drive.base.presentation.extension.getDefaultMessage
+import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
+import javax.inject.Inject
+
+@Suppress("StaticFieldLeak")
+@HiltViewModel
+class UnlockViewModel @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val signOut: SignOut,
+ private val unlockApp: UnlockApp,
+ private val configurationProvider: ConfigurationProvider,
+ private val broadcastMessages: BroadcastMessages,
+ private val accountManager: AccountManager,
+) : ViewModel() {
+
+ val viewEvent = object : UnlockViewEvent {
+ override val onShowBiometric: () -> Unit = { showBiometrics() }
+ override val onSignOut: (userId: UserId?) -> Unit = { userId -> userId?.let { doSignOut(userId) }}
+ }
+
+ private fun showBiometrics() = viewModelScope.launch {
+ unlockApp()
+ .onFailure { error ->
+ broadcastMessages(
+ userId = accountManager.getAccounts().first().first().userId,
+ message = when (error) {
+ is LockException -> error.getDefaultMessage(appContext)
+ else -> error.getDefaultMessage(appContext, configurationProvider.useExceptionMessage)
+ },
+ type = BroadcastMessage.Type.WARNING,
+ )
+ }
+ }
+
+ private fun doSignOut(userId: UserId) = viewModelScope.launch {
+ signOut(userId)
+ }
+}
diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark.webp
new file mode 100644
index 00000000..ff4acd8d
Binary files /dev/null and b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark.webp differ
diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark_land.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark_land.webp
new file mode 100644
index 00000000..93fc8598
Binary files /dev/null and b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_dark_land.webp differ
diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light.webp
new file mode 100644
index 00000000..4329da25
Binary files /dev/null and b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light.webp differ
diff --git a/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light_land.webp b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light_land.webp
new file mode 100644
index 00000000..5cb8502b
Binary files /dev/null and b/app-lock/presentation/src/main/res/drawable-nodpi/welcome_header_light_land.webp differ
diff --git a/app-lock/presentation/src/main/res/values-land-night/drawable.xml b/app-lock/presentation/src/main/res/values-land-night/drawable.xml
new file mode 100644
index 00000000..f0553bb3
--- /dev/null
+++ b/app-lock/presentation/src/main/res/values-land-night/drawable.xml
@@ -0,0 +1,21 @@
+
+
+
+ - @drawable/welcome_header_dark_land
+
diff --git a/app-lock/presentation/src/main/res/values-land/drawable.xml b/app-lock/presentation/src/main/res/values-land/drawable.xml
new file mode 100644
index 00000000..c680ff7d
--- /dev/null
+++ b/app-lock/presentation/src/main/res/values-land/drawable.xml
@@ -0,0 +1,23 @@
+
+
+
+ - @drawable/welcome_header_light_land
+ - @drawable/welcome_header_light_land
+ - @drawable/welcome_header_dark_land
+
diff --git a/app-lock/presentation/src/main/res/values-night/drawable.xml b/app-lock/presentation/src/main/res/values-night/drawable.xml
new file mode 100644
index 00000000..7b219490
--- /dev/null
+++ b/app-lock/presentation/src/main/res/values-night/drawable.xml
@@ -0,0 +1,21 @@
+
+
+
+ - @drawable/welcome_header_dark
+
diff --git a/app-lock/presentation/src/main/res/values/drawable.xml b/app-lock/presentation/src/main/res/values/drawable.xml
new file mode 100644
index 00000000..85250111
--- /dev/null
+++ b/app-lock/presentation/src/main/res/values/drawable.xml
@@ -0,0 +1,21 @@
+
+
+
+ - @drawable/welcome_header_light
+
diff --git a/app-lock/src/main/AndroidManifest.xml b/app-lock/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..707ffdcb
--- /dev/null
+++ b/app-lock/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a00aea3f..96080a54 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -16,7 +16,6 @@
* along with Proton Drive. If not, see .
*/
-import java.io.FileInputStream
import java.util.Properties
plugins {
@@ -49,6 +48,7 @@ driveModule(
serialization = true,
) {
implementation(files("../../proton-libs/gopenpgp/gopenpgp.aar"))
+ implementation(project(":app-lock"))
implementation(project(":app-ui-settings"))
implementation(project(":drive"))
@@ -67,13 +67,14 @@ driveModule(
implementation(libs.timber)
androidTestImplementation(libs.androidx.navigation.compose)
+ androidTestImplementation(libs.fusion)
coreLibraryDesugaring(libs.desugar.jdk.libs)
}
val privateProperties = Properties().apply {
try {
- load(FileInputStream("private.properties"))
+ load(rootDir.resolve("private.properties").inputStream())
} catch (exception: java.io.FileNotFoundException) {
// Provide empty properties to allow the app to be built without secrets
logger.warn("private.properties file not found", exception)
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/1.json b/app/schemas/me.proton.android.drive.db.AppDatabase/1.json
index 8d06834d..995a4108 100644
--- a/app/schemas/me.proton.android.drive.db.AppDatabase/1.json
+++ b/app/schemas/me.proton.android.drive.db.AppDatabase/1.json
@@ -2,1662 +2,12 @@
"formatVersion": 1,
"database": {
"version": 1,
- "identityHash": "1848c5aa24ef749bea72f819f081cfe5",
+ "identityHash": "be4e11f65691008e811c02bbbb906104",
"entities": [
{
- "tableName": "AccountEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "tableName": "AppLockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`key`))",
"fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "username",
- "columnName": "username",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "email",
- "columnName": "email",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "sessionId",
- "columnName": "sessionId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "sessionState",
- "columnName": "sessionState",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_AccountEntity_sessionId",
- "unique": false,
- "columnNames": [
- "sessionId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
- },
- {
- "name": "index_AccountEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "SessionEntity",
- "onDelete": "NO ACTION",
- "onUpdate": "NO ACTION",
- "columns": [
- "sessionId"
- ],
- "referencedColumns": [
- "sessionId"
- ]
- }
- ]
- },
- {
- "tableName": "AccountMetadataEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "product",
- "columnName": "product",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "primaryAtUtc",
- "columnName": "primaryAtUtc",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "migrations",
- "columnName": "migrations",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId",
- "product"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_AccountMetadataEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- },
- {
- "name": "index_AccountMetadataEntity_product",
- "unique": false,
- "columnNames": [
- "product"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)"
- },
- {
- "name": "index_AccountMetadataEntity_primaryAtUtc",
- "unique": false,
- "columnNames": [
- "primaryAtUtc"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "SessionEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "sessionId",
- "columnName": "sessionId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "accessToken",
- "columnName": "accessToken",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "refreshToken",
- "columnName": "refreshToken",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "scopes",
- "columnName": "scopes",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "product",
- "columnName": "product",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "sessionId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_SessionEntity_sessionId",
- "unique": false,
- "columnNames": [
- "sessionId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
- },
- {
- "name": "index_SessionEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "SessionDetailsEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "sessionId",
- "columnName": "sessionId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "initialEventId",
- "columnName": "initialEventId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "requiredAccountType",
- "columnName": "requiredAccountType",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "secondFactorEnabled",
- "columnName": "secondFactorEnabled",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "twoPassModeEnabled",
- "columnName": "twoPassModeEnabled",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "password",
- "columnName": "password",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "sessionId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_SessionDetailsEntity_sessionId",
- "unique": false,
- "columnNames": [
- "sessionId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "SessionEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "sessionId"
- ],
- "referencedColumns": [
- "sessionId"
- ]
- }
- ]
- },
- {
- "tableName": "UserEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "email",
- "columnName": "email",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "displayName",
- "columnName": "displayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "currency",
- "columnName": "currency",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "credit",
- "columnName": "credit",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "usedSpace",
- "columnName": "usedSpace",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "maxSpace",
- "columnName": "maxSpace",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "maxUpload",
- "columnName": "maxUpload",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "role",
- "columnName": "role",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "private",
- "columnName": "private",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "subscribed",
- "columnName": "subscribed",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "services",
- "columnName": "services",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "delinquent",
- "columnName": "delinquent",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "passphrase",
- "columnName": "passphrase",
- "affinity": "BLOB",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_UserEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "UserKeyEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "keyId",
- "columnName": "keyId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "version",
- "columnName": "version",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "privateKey",
- "columnName": "privateKey",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "isPrimary",
- "columnName": "isPrimary",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "isUnlockable",
- "columnName": "isUnlockable",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "fingerprint",
- "columnName": "fingerprint",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "activation",
- "columnName": "activation",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "active",
- "columnName": "active",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "keyId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_UserKeyEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- },
- {
- "name": "index_UserKeyEntity_keyId",
- "unique": false,
- "columnNames": [
- "keyId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "UserEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "AddressEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "addressId",
- "columnName": "addressId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "email",
- "columnName": "email",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "displayName",
- "columnName": "displayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "signature",
- "columnName": "signature",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "domainId",
- "columnName": "domainId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "canSend",
- "columnName": "canSend",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "canReceive",
- "columnName": "canReceive",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "enabled",
- "columnName": "enabled",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "order",
- "columnName": "order",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "signedKeyList.data",
- "columnName": "signedKeyList_data",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "signedKeyList.signature",
- "columnName": "signedKeyList_signature",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "addressId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_AddressEntity_addressId",
- "unique": false,
- "columnNames": [
- "addressId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
- },
- {
- "name": "index_AddressEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "UserEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "AddressKeyEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "addressId",
- "columnName": "addressId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "keyId",
- "columnName": "keyId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "version",
- "columnName": "version",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "privateKey",
- "columnName": "privateKey",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "isPrimary",
- "columnName": "isPrimary",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "isUnlockable",
- "columnName": "isUnlockable",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "flags",
- "columnName": "flags",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "passphrase",
- "columnName": "passphrase",
- "affinity": "BLOB",
- "notNull": false
- },
- {
- "fieldPath": "token",
- "columnName": "token",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "signature",
- "columnName": "signature",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "fingerprint",
- "columnName": "fingerprint",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "fingerprints",
- "columnName": "fingerprints",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "activation",
- "columnName": "activation",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "active",
- "columnName": "active",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "keyId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_AddressKeyEntity_addressId",
- "unique": false,
- "columnNames": [
- "addressId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
- },
- {
- "name": "index_AddressKeyEntity_keyId",
- "unique": false,
- "columnNames": [
- "keyId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AddressEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "addressId"
- ],
- "referencedColumns": [
- "addressId"
- ]
- }
- ]
- },
- {
- "tableName": "KeySaltEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "keyId",
- "columnName": "keyId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "keySalt",
- "columnName": "keySalt",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId",
- "keyId"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_KeySaltEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- },
- {
- "name": "index_KeySaltEntity_keyId",
- "unique": false,
- "columnNames": [
- "keyId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
- }
- ],
- "foreignKeys": []
- },
- {
- "tableName": "PublicAddressEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))",
- "fields": [
- {
- "fieldPath": "email",
- "columnName": "email",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "recipientType",
- "columnName": "recipientType",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "mimeType",
- "columnName": "mimeType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "signedKeyListEntity.data",
- "columnName": "signedKeyList_data",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "signedKeyListEntity.signature",
- "columnName": "signedKeyList_signature",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "email"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_PublicAddressEntity_email",
- "unique": false,
- "columnNames": [
- "email"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)"
- }
- ],
- "foreignKeys": []
- },
- {
- "tableName": "PublicAddressKeyEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "email",
- "columnName": "email",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "flags",
- "columnName": "flags",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "publicKey",
- "columnName": "publicKey",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "isPrimary",
- "columnName": "isPrimary",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "email",
- "publicKey"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_PublicAddressKeyEntity_email",
- "unique": false,
- "columnNames": [
- "email"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "PublicAddressEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "email"
- ],
- "referencedColumns": [
- "email"
- ]
- }
- ]
- },
- {
- "tableName": "HumanVerificationEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `captchaVerificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))",
- "fields": [
- {
- "fieldPath": "clientId",
- "columnName": "clientId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "clientIdType",
- "columnName": "clientIdType",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "verificationMethods",
- "columnName": "verificationMethods",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "captchaVerificationToken",
- "columnName": "captchaVerificationToken",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "humanHeaderTokenType",
- "columnName": "humanHeaderTokenType",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "humanHeaderTokenCode",
- "columnName": "humanHeaderTokenCode",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "clientId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": []
- },
- {
- "tableName": "UserSettingsEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `twoFA_u2fKeys` TEXT, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "news",
- "columnName": "news",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "locale",
- "columnName": "locale",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "logAuth",
- "columnName": "logAuth",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "invoiceText",
- "columnName": "invoiceText",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "density",
- "columnName": "density",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "theme",
- "columnName": "theme",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "themeType",
- "columnName": "themeType",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "weekStart",
- "columnName": "weekStart",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "dateFormat",
- "columnName": "dateFormat",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "timeFormat",
- "columnName": "timeFormat",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "welcome",
- "columnName": "welcome",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "earlyAccess",
- "columnName": "earlyAccess",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "email.value",
- "columnName": "email_value",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "email.status",
- "columnName": "email_status",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "email.notify",
- "columnName": "email_notify",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "email.reset",
- "columnName": "email_reset",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "phone.value",
- "columnName": "phone_value",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "phone.status",
- "columnName": "phone_status",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "phone.notify",
- "columnName": "phone_notify",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "phone.reset",
- "columnName": "phone_reset",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "password.mode",
- "columnName": "password_mode",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "password.expirationTime",
- "columnName": "password_expirationTime",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "twoFA.enabled",
- "columnName": "twoFA_enabled",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "twoFA.allowed",
- "columnName": "twoFA_allowed",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "twoFA.expirationTime",
- "columnName": "twoFA_expirationTime",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "twoFA.u2fKeys",
- "columnName": "twoFA_u2fKeys",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "flags.welcomed",
- "columnName": "flags_welcomed",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "UserEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "OrganizationEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "displayName",
- "columnName": "displayName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "planName",
- "columnName": "planName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "vpnPlanName",
- "columnName": "vpnPlanName",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "twoFactorGracePeriod",
- "columnName": "twoFactorGracePeriod",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "theme",
- "columnName": "theme",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "email",
- "columnName": "email",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "maxDomains",
- "columnName": "maxDomains",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "maxAddresses",
- "columnName": "maxAddresses",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "maxSpace",
- "columnName": "maxSpace",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "maxMembers",
- "columnName": "maxMembers",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "maxVPN",
- "columnName": "maxVPN",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "features",
- "columnName": "features",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "flags",
- "columnName": "flags",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "usedDomains",
- "columnName": "usedDomains",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "usedAddresses",
- "columnName": "usedAddresses",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "usedSpace",
- "columnName": "usedSpace",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "assignedSpace",
- "columnName": "assignedSpace",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "usedMembers",
- "columnName": "usedMembers",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "usedVPN",
- "columnName": "usedVPN",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "hasKeys",
- "columnName": "hasKeys",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "toMigrate",
- "columnName": "toMigrate",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "UserEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "OrganizationKeysEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "publicKey",
- "columnName": "publicKey",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "privateKey",
- "columnName": "privateKey",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "UserEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "EventMetadataEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "userId",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "config",
- "columnName": "config",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "eventId",
- "columnName": "eventId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "nextEventId",
- "columnName": "nextEventId",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "refresh",
- "columnName": "refresh",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "more",
- "columnName": "more",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "response",
- "columnName": "response",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "retry",
- "columnName": "retry",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "createdAt",
- "columnName": "createdAt",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "updatedAt",
- "columnName": "updatedAt",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "userId",
- "config"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_EventMetadataEntity_userId",
- "unique": false,
- "columnNames": [
- "userId"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
- },
- {
- "name": "index_EventMetadataEntity_config",
- "unique": false,
- "columnNames": [
- "config"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)"
- },
- {
- "name": "index_EventMetadataEntity_createdAt",
- "unique": false,
- "columnNames": [
- "createdAt"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "UserEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "userId"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "VolumeEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "creationTime",
- "columnName": "creation_time",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "maxSpace",
- "columnName": "max_space",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "usedSpace",
- "columnName": "used_space",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "INTEGER",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_VolumeEntity_user_id",
- "unique": false,
- "columnNames": [
- "user_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
- },
- {
- "name": "index_VolumeEntity_share_id",
- "unique": false,
- "columnNames": [
- "share_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
- },
- {
- "name": "index_VolumeEntity_id",
- "unique": false,
- "columnNames": [
- "id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "ShareEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `block_size` INTEGER NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "volumeId",
- "columnName": "volume_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "flags",
- "columnName": "flags",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "blockSize",
- "columnName": "block_size",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "isLocked",
- "columnName": "locked",
- "affinity": "INTEGER",
- "notNull": true
- },
{
"fieldPath": "key",
"columnName": "key",
@@ -1665,1569 +15,132 @@
"notNull": true
},
{
- "fieldPath": "passphrase",
- "columnName": "passphrase",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "passphraseSignature",
- "columnName": "passphrase_signature",
+ "fieldPath": "type",
+ "columnName": "type",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
- "user_id",
- "id"
+ "key"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "LockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`passphrase` TEXT NOT NULL, `key` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`passphrase`), FOREIGN KEY(`key`) REFERENCES `AppLockEntity`(`key`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "appKeyPassphrase",
+ "columnName": "passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "appKey",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "passphrase"
],
"autoGenerate": false
},
"indices": [
{
- "name": "index_ShareEntity_user_id",
+ "name": "index_LockEntity_key",
"unique": false,
"columnNames": [
- "user_id"
+ "key"
],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
- },
- {
- "name": "index_ShareEntity_volume_id",
- "unique": false,
- "columnNames": [
- "volume_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
- },
- {
- "name": "index_ShareEntity_link_id",
- "unique": false,
- "columnNames": [
- "link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LockEntity_key` ON `${TABLE_NAME}` (`key`)"
}
],
"foreignKeys": [
{
- "table": "AccountEntity",
+ "table": "AppLockEntity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
- "user_id"
+ "key"
],
"referencedColumns": [
- "userId"
+ "key"
]
}
]
},
{
- "tableName": "ShareUrlEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "tableName": "AutoLockDurationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `duration` INTEGER NOT NULL, PRIMARY KEY(`key`))",
"fields": [
{
- "fieldPath": "id",
- "columnName": "id",
+ "fieldPath": "key",
+ "columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "flags",
- "columnName": "flags",
+ "fieldPath": "durationInSeconds",
+ "columnName": "duration",
"affinity": "INTEGER",
"notNull": true
- },
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "key"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "EnableAppLockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `last_access_time` INTEGER NOT NULL, PRIMARY KEY(`key`))",
+ "fields": [
{
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "token",
- "columnName": "token",
+ "fieldPath": "key",
+ "columnName": "key",
"affinity": "TEXT",
"notNull": true
},
{
- "fieldPath": "creatorEmail",
- "columnName": "creatior_email",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "permissions",
- "columnName": "permissions",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "creationTime",
- "columnName": "creation_time",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "expirationTime",
- "columnName": "expiration_time",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "lastAccessTime",
+ "fieldPath": "timestamp",
"columnName": "last_access_time",
"affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "maxAccesses",
- "columnName": "max_accesses",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "numberOfAccesses",
- "columnName": "number_of_accesses",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "urlPasswordSalt",
- "columnName": "url_password_salt",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "sharePasswordSalt",
- "columnName": "share_password_salt",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "srpVerifier",
- "columnName": "srp_verifier",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "srpModulusId",
- "columnName": "srp_modulus_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "password",
- "columnName": "password",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "sharePassphraseKeyPacket",
- "columnName": "share_passphrase_key_packet",
- "affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
- "user_id",
- "share_id",
- "id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_ShareUrlEntity_user_id_share_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "ShareEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id"
- ],
- "referencedColumns": [
- "user_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "LinkEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "parentId",
- "columnName": "parent_id",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "type",
- "columnName": "type",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nameSignatureEmail",
- "columnName": "name_signature_email",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "hash",
- "columnName": "hash",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "expirationTime",
- "columnName": "expiration_time",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "size",
- "columnName": "size",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "mimeType",
- "columnName": "mime_type",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "attributes",
- "columnName": "attributes",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "permissions",
- "columnName": "permissions",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "nodeKey",
- "columnName": "node_key",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nodePassphrase",
- "columnName": "node_passphrase",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nodePassphraseSignature",
- "columnName": "node_passphrase_signature",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "signatureAddress",
- "columnName": "signature_address",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "creationTime",
- "columnName": "creation_time",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "lastModified",
- "columnName": "last_modified",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "trashedTime",
- "columnName": "trashed_time",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "shared",
- "columnName": "is_shared",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "numberOfAccesses",
- "columnName": "number_of_accesses",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "shareUrlExpirationTime",
- "columnName": "share_url_expiration_time",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_LinkEntity_user_id_share_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
- },
- {
- "name": "index_LinkEntity_user_id_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)"
- },
- {
- "name": "index_LinkEntity_user_id_share_id_parent_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id",
- "parent_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- },
- {
- "table": "ShareEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id"
- ],
- "referencedColumns": [
- "user_id",
- "id"
- ]
- },
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "parent_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "LinkFilePropertiesEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "file_user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "file_share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "file_link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "activeRevisionId",
- "columnName": "revision_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "hasThumbnail",
- "columnName": "has_thumbnail",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "contentKeyPacket",
- "columnName": "content_key_packet",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "contentKeyPacketSignature",
- "columnName": "content_key_packet_signature",
- "affinity": "TEXT",
- "notNull": false
- },
- {
- "fieldPath": "activeRevisionSignatureAddress",
- "columnName": "file_signature_address",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "file_user_id",
- "file_share_id",
- "file_link_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id",
- "unique": false,
- "columnNames": [
- "file_user_id",
- "file_share_id",
- "file_link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)"
- },
- {
- "name": "index_LinkFilePropertiesEntity_revision_id",
- "unique": false,
- "columnNames": [
- "revision_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "file_user_id",
- "file_share_id",
- "file_link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "LinkFolderPropertiesEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "folder_user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "folder_share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "folder_link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nodeHashKey",
- "columnName": "node_hash_key",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "folder_user_id",
- "folder_share_id",
- "folder_link_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id",
- "unique": false,
- "columnNames": [
- "folder_user_id",
- "folder_share_id",
- "folder_link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "folder_user_id",
- "folder_share_id",
- "folder_link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "LinkOfflineEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_LinkOfflineEntity_user_id_share_id_link_id",
- "unique": true,
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "LinkDownloadStateEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "revisionId",
- "columnName": "revision_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "link_id",
- "revision_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "DownloadBlockEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "revisionId",
- "columnName": "revision_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "index",
- "columnName": "index",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "uri",
- "columnName": "uri",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "encryptedSignature",
- "columnName": "encrypted_signature",
- "affinity": "TEXT",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "link_id",
- "revision_id",
- "index"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id",
- "link_id",
- "revision_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkDownloadStateEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id",
- "revision_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "link_id",
- "revision_id"
- ]
- }
- ]
- },
- {
- "tableName": "LinkTrashStateEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_LinkTrashStateEntity_user_id_share_id_link_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "TrashWorkEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "workId",
- "columnName": "work_id",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_TrashWorkEntity_user_id_share_id_link_id",
- "unique": false,
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
- },
- {
- "name": "index_TrashWorkEntity_work_id",
- "unique": false,
- "columnNames": [
- "work_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "MessageEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "content",
- "columnName": "content",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "id"
- ],
- "autoGenerate": true
- },
- "indices": [
- {
- "name": "index_MessageEntity_user_id",
- "unique": false,
- "columnNames": [
- "user_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "UiSettingsEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "layoutType",
- "columnName": "layout_type",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "themeStyle",
- "columnName": "theme_style",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id"
+ "key"
],
"autoGenerate": false
},
"indices": [],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "DriveLinkRemoteKeyEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "key",
- "columnName": "key",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "prevKey",
- "columnName": "previous_key",
- "affinity": "INTEGER",
- "notNull": false
- },
- {
- "fieldPath": "nextKey",
- "columnName": "next_key",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "key",
- "user_id",
- "share_id",
- "link_id"
- ],
- "autoGenerate": false
- },
- "indices": [
- {
- "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id",
- "unique": true,
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
- },
- {
- "name": "index_DriveLinkRemoteKeyEntity_user_id",
- "unique": false,
- "columnNames": [
- "user_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- },
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "SortingEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "sortingBy",
- "columnName": "sorting_by",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "sortingDirection",
- "columnName": "sorting_direction",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "LinkUploadEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "id",
- "columnName": "id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "volumeId",
- "columnName": "volume_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "parentId",
- "columnName": "parent_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "revisionId",
- "columnName": "revision_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "name",
- "columnName": "name",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "mimeType",
- "columnName": "mime_type",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nodeKey",
- "columnName": "node_key",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nodePassphrase",
- "columnName": "node_passphrase",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "nodePassphraseSignature",
- "columnName": "node_passphrase_signature",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "contentKeyPacket",
- "columnName": "content_key_packet",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "contentKeyPacketSignature",
- "columnName": "content_key_packet_signature",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "manifestSignature",
- "columnName": "manifest_signature",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "state",
- "columnName": "state",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "id"
- ],
- "autoGenerate": true
- },
- "indices": [
- {
- "name": "index_LinkUploadEntity_user_id",
- "unique": false,
- "columnNames": [
- "user_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
- },
- {
- "name": "index_LinkUploadEntity_volume_id",
- "unique": false,
- "columnNames": [
- "volume_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
- },
- {
- "name": "index_LinkUploadEntity_share_id",
- "unique": false,
- "columnNames": [
- "share_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
- },
- {
- "name": "index_LinkUploadEntity_link_id",
- "unique": false,
- "columnNames": [
- "link_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
- },
- {
- "name": "index_LinkUploadEntity_revision_id",
- "unique": false,
- "columnNames": [
- "revision_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
- },
- {
- "name": "index_LinkUploadEntity_parent_id",
- "unique": false,
- "columnNames": [
- "parent_id"
- ],
- "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
- }
- ],
- "foreignKeys": [
- {
- "table": "AccountEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id"
- ],
- "referencedColumns": [
- "userId"
- ]
- }
- ]
- },
- {
- "tableName": "UploadBlockEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "uploadLinkId",
- "columnName": "upload_link_id",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "index",
- "columnName": "index",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "size",
- "columnName": "size",
- "affinity": "INTEGER",
- "notNull": true
- },
- {
- "fieldPath": "encryptedSignature",
- "columnName": "encrypted_signature",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "hash",
- "columnName": "hash",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "uploadToken",
- "columnName": "token",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "url",
- "columnName": "url",
- "affinity": "TEXT",
- "notNull": true
- }
- ],
- "primaryKey": {
- "columnNames": [
- "upload_link_id",
- "index"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "LinkUploadEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "upload_link_id"
- ],
- "referencedColumns": [
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "FolderMetadataEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "linkId",
- "columnName": "link_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "lastFetchChildrenTimestamp",
- "columnName": "last_fetch_children_timestamp",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "LinkEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id",
- "link_id"
- ],
- "referencedColumns": [
- "user_id",
- "share_id",
- "id"
- ]
- }
- ]
- },
- {
- "tableName": "TrashMetadataEntity",
- "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
- "fields": [
- {
- "fieldPath": "userId",
- "columnName": "user_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "shareId",
- "columnName": "share_id",
- "affinity": "TEXT",
- "notNull": true
- },
- {
- "fieldPath": "lastFetchTrashTimestamp",
- "columnName": "last_fetch_trash_timestamp",
- "affinity": "INTEGER",
- "notNull": false
- }
- ],
- "primaryKey": {
- "columnNames": [
- "user_id",
- "share_id"
- ],
- "autoGenerate": false
- },
- "indices": [],
- "foreignKeys": [
- {
- "table": "ShareEntity",
- "onDelete": "CASCADE",
- "onUpdate": "NO ACTION",
- "columns": [
- "user_id",
- "share_id"
- ],
- "referencedColumns": [
- "user_id",
- "id"
- ]
- }
- ]
+ "foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1848c5aa24ef749bea72f819f081cfe5')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be4e11f65691008e811c02bbbb906104')"
]
}
}
\ No newline at end of file
diff --git a/app/src/dynamic/AndroidManifest.xml b/app/src/dynamic/AndroidManifest.xml
new file mode 100644
index 00000000..15a9aa08
--- /dev/null
+++ b/app/src/dynamic/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e449c412..ee360a1d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -126,6 +126,17 @@
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
+
+
+
@@ -136,9 +147,18 @@
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
+
+ android:value="androidx.startup"
+ tools:node="remove" />
+
@@ -147,7 +167,16 @@
android:value="androidx.startup" />
+ android:value="androidx.startup"
+ tools:node="remove" />
+
+
diff --git a/app/src/main/kotlin/me/proton/android/drive/App.kt b/app/src/main/kotlin/me/proton/android/drive/App.kt
index 02fa8345..b5485e41 100644
--- a/app/src/main/kotlin/me/proton/android/drive/App.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/App.kt
@@ -20,6 +20,12 @@ package me.proton.android.drive
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
+import me.proton.android.drive.initializer.MainInitializer
@HiltAndroidApp
-class App : Application()
+class App : Application() {
+ override fun onCreate() {
+ super.onCreate()
+ MainInitializer.init(this)
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt b/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt
index 35e5880c..a697010d 100644
--- a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/db/AppDatabase.kt
@@ -19,247 +19,33 @@
package me.proton.android.drive.db
import android.content.Context
-import androidx.room.AutoMigration
import androidx.room.Database
-import androidx.room.TypeConverters
-import me.proton.core.account.data.db.AccountConverters
-import me.proton.core.account.data.db.AccountDatabase
-import me.proton.core.account.data.entity.AccountEntity
-import me.proton.core.account.data.entity.AccountMetadataEntity
-import me.proton.core.account.data.entity.SessionDetailsEntity
-import me.proton.core.account.data.entity.SessionEntity
-import me.proton.core.challenge.data.db.ChallengeConverters
-import me.proton.core.challenge.data.db.ChallengeDatabase
-import me.proton.core.challenge.data.entity.ChallengeFrameEntity
-import me.proton.core.crypto.android.keystore.CryptoConverters
+import androidx.room.migration.Migration
+import me.proton.android.drive.lock.data.db.AppLockDatabase
+import me.proton.android.drive.lock.data.db.entity.AppLockEntity
+import me.proton.android.drive.lock.data.db.entity.AutoLockDurationEntity
+import me.proton.android.drive.lock.data.db.entity.EnableAppLockEntity
+import me.proton.android.drive.lock.data.db.entity.LockEntity
import me.proton.core.data.room.db.BaseDatabase
-import me.proton.core.data.room.db.CommonConverters
-import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase
-import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase
-import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase
-import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase
-import me.proton.core.drive.drivelink.paged.data.db.entity.DriveLinkRemoteKeyEntity
-import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase
-import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase
-import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase
-import me.proton.core.drive.folder.data.db.FolderDatabase
-import me.proton.core.drive.folder.data.db.FolderMetadataEntity
-import me.proton.core.drive.link.data.db.LinkDatabase
-import me.proton.core.drive.link.data.db.entity.LinkEntity
-import me.proton.core.drive.link.data.db.entity.LinkFilePropertiesEntity
-import me.proton.core.drive.link.data.db.entity.LinkFolderPropertiesEntity
-import me.proton.core.drive.link.selection.data.db.LinkSelectionConverters
-import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase
-import me.proton.core.drive.link.selection.data.db.entity.LinkSelectionEntity
-import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase
-import me.proton.core.drive.linkdownload.data.db.entity.DownloadBlockEntity
-import me.proton.core.drive.linkdownload.data.db.entity.LinkDownloadStateEntity
-import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase
-import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase
-import me.proton.core.drive.linkoffline.data.db.LinkOfflineEntity
-import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase
-import me.proton.core.drive.linktrash.data.db.entity.LinkTrashStateEntity
-import me.proton.core.drive.linktrash.data.db.entity.TrashMetadataEntity
-import me.proton.core.drive.linktrash.data.db.entity.TrashWorkEntity
-import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase
-import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity
-import me.proton.core.drive.linkupload.data.db.entity.UploadBlockEntity
-import me.proton.core.drive.linkupload.data.db.entity.UploadBulkEntity
-import me.proton.core.drive.linkupload.data.db.entity.UploadBulkUriStringEntity
-import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase
-import me.proton.core.drive.messagequeue.data.storage.db.entity.MessageEntity
-import me.proton.core.drive.notification.data.db.NotificationConverters
-import me.proton.core.drive.notification.data.db.NotificationDatabase
-import me.proton.core.drive.notification.data.db.entity.NotificationChannelEntity
-import me.proton.core.drive.notification.data.db.entity.NotificationEventEntity
-import me.proton.core.drive.share.data.db.ShareDatabase
-import me.proton.core.drive.share.data.db.ShareEntity
-import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase
-import me.proton.core.drive.shareurl.base.data.db.entity.ShareUrlEntity
-import me.proton.core.drive.sorting.data.db.SortingDatabase
-import me.proton.core.drive.sorting.data.db.entity.SortingEntity
-import me.proton.core.drive.volume.data.db.VolumeDatabase
-import me.proton.core.drive.volume.data.db.VolumeEntity
-import me.proton.core.eventmanager.data.db.EventManagerConverters
-import me.proton.core.eventmanager.data.db.EventMetadataDatabase
-import me.proton.core.eventmanager.data.entity.EventMetadataEntity
-import me.proton.core.featureflag.data.db.FeatureFlagDatabase
-import me.proton.core.featureflag.data.entity.FeatureFlagEntity
-import me.proton.core.humanverification.data.db.HumanVerificationConverters
-import me.proton.core.humanverification.data.db.HumanVerificationDatabase
-import me.proton.core.humanverification.data.entity.HumanVerificationEntity
-import me.proton.core.key.data.db.KeySaltDatabase
-import me.proton.core.key.data.db.PublicAddressDatabase
-import me.proton.core.key.data.entity.KeySaltEntity
-import me.proton.core.key.data.entity.PublicAddressEntity
-import me.proton.core.key.data.entity.PublicAddressKeyEntity
-import me.proton.core.payment.data.local.db.PaymentDatabase
-import me.proton.core.payment.data.local.entity.GooglePurchaseEntity
-import me.proton.core.user.data.db.AddressDatabase
-import me.proton.core.user.data.db.UserConverters
-import me.proton.core.user.data.db.UserDatabase
-import me.proton.core.user.data.entity.AddressEntity
-import me.proton.core.user.data.entity.AddressKeyEntity
-import me.proton.core.user.data.entity.UserEntity
-import me.proton.core.user.data.entity.UserKeyEntity
-import me.proton.core.usersettings.data.db.OrganizationDatabase
-import me.proton.core.usersettings.data.db.UserSettingsConverters
-import me.proton.core.usersettings.data.db.UserSettingsDatabase
-import me.proton.core.usersettings.data.entity.OrganizationEntity
-import me.proton.core.usersettings.data.entity.OrganizationKeysEntity
-import me.proton.core.usersettings.data.entity.UserSettingsEntity
-import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase
-import me.proton.drive.android.settings.data.db.entity.UiSettingsEntity
@Database(
entities = [
- // Core
- AccountEntity::class,
- AccountMetadataEntity::class,
- SessionEntity::class,
- SessionDetailsEntity::class,
- UserEntity::class,
- UserKeyEntity::class,
- AddressEntity::class,
- AddressKeyEntity::class,
- KeySaltEntity::class,
- PublicAddressEntity::class,
- PublicAddressKeyEntity::class,
- HumanVerificationEntity::class,
- UserSettingsEntity::class,
- OrganizationEntity::class,
- OrganizationKeysEntity::class,
- EventMetadataEntity::class,
- FeatureFlagEntity::class,
- ChallengeFrameEntity::class,
- GooglePurchaseEntity::class,
- // Drive
- VolumeEntity::class,
- ShareEntity::class,
- ShareUrlEntity::class,
- LinkEntity::class,
- LinkFilePropertiesEntity::class,
- LinkFolderPropertiesEntity::class,
- LinkOfflineEntity::class,
- LinkDownloadStateEntity::class,
- DownloadBlockEntity::class,
- LinkTrashStateEntity::class,
- // Trash
- TrashWorkEntity::class,
- // MessageQueue
- MessageEntity::class,
- // AppUiSettings
- UiSettingsEntity::class,
- // DriveLinkPaged
- DriveLinkRemoteKeyEntity::class,
- // Sorting
- SortingEntity::class,
- // Upload
- LinkUploadEntity::class,
- UploadBlockEntity::class,
- UploadBulkEntity::class,
- UploadBulkUriStringEntity::class,
- FolderMetadataEntity::class,
- TrashMetadataEntity::class,
- // Notification
- NotificationChannelEntity::class,
- NotificationEventEntity::class,
- // Selection
- LinkSelectionEntity::class,
+ // AppLock
+ AppLockEntity::class,
+ LockEntity::class,
+ AutoLockDurationEntity::class,
+ EnableAppLockEntity::class,
],
version = AppDatabase.VERSION,
- autoMigrations = [
- AutoMigration(from = 4, to = 5),
- AutoMigration(from = 5, to = 6),
- AutoMigration(from = 7, to = 8),
- AutoMigration(from = 9, to = 10),
- AutoMigration(from = 13, to = 14),
- AutoMigration(from = 15, to = 16),
- AutoMigration(from = 16, to = 17),
- AutoMigration(from = 17, to = 18, spec = ShareDatabase.DeleteBlockSizeFromShareEntity::class),
- AutoMigration(from = 18, to = 19),
- ],
- exportSchema = true,
)
-@TypeConverters(
- // Core
- CommonConverters::class,
- AccountConverters::class,
- UserConverters::class,
- CryptoConverters::class,
- HumanVerificationConverters::class,
- UserSettingsConverters::class,
- EventManagerConverters::class,
- ChallengeConverters::class,
- // Drive
- NotificationConverters::class,
- LinkSelectionConverters::class,
-)
-abstract class AppDatabase :
- BaseDatabase(),
- AccountDatabase,
- UserDatabase,
- AddressDatabase,
- KeySaltDatabase,
- HumanVerificationDatabase,
- PublicAddressDatabase,
- UserSettingsDatabase,
- OrganizationDatabase,
- FeatureFlagDatabase,
- VolumeDatabase,
- ShareDatabase,
- ShareUrlDatabase,
- LinkDatabase,
- FolderDatabase,
- LinkAncestorDatabase,
- LinkOfflineDatabase,
- LinkDownloadDatabase,
- LinkTrashDatabase,
- LinkSelectionDatabase,
- MessageQueueDatabase,
- AppUiSettingsDatabase,
- EventMetadataDatabase,
- ChallengeDatabase,
- SortingDatabase,
- LinkUploadDatabase,
- DriveLinkDatabase,
- DriveLinkPagedDatabase,
- DriveLinkTrashDatabase,
- DriveLinkOfflineDatabase,
- DriveLinkDownloadDatabase,
- DriveLinkSharedDatabase,
- DriveLinkSelectionDatabase,
- NotificationDatabase,
- PaymentDatabase {
+abstract class AppDatabase : BaseDatabase(), AppLockDatabase {
companion object {
- const val VERSION = 21
-
- private val migrations = listOf(
- AppDatabaseMigrations.MIGRATION_1_2,
- AppDatabaseMigrations.MIGRATION_2_3,
- AppDatabaseMigrations.MIGRATION_3_4,
- //AutoMigration(from = 4, to = 5)
- //AutoMigration(from = 5, to = 6)
- AppDatabaseMigrations.MIGRATION_6_7,
- //AutoMigration(from = 7, to = 8)
- AppDatabaseMigrations.MIGRATION_8_9,
- //AutoMigration(from = 9, to = 10)
- AppDatabaseMigrations.MIGRATION_10_11,
- AppDatabaseMigrations.MIGRATION_11_12,
- AppDatabaseMigrations.MIGRATION_12_13,
- //AutoMigration(from = 13, to = 14)
- AppDatabaseMigrations.MIGRATION_14_15,
- //AutoMigration(from = 15, to = 16)
- //AutoMigration(from = 16, to = 17)
- //AutoMigration(from = 17, to = 18)
- //AutoMigration(from = 18, to = 19)
- AppDatabaseMigrations.MIGRATION_19_20,
- AppDatabaseMigrations.MIGRATION_20_21,
- )
+ const val VERSION = 1
+ private val migrations = listOf()
fun buildDatabase(context: Context): AppDatabase =
- databaseBuilder(context, "db-drive")
+ databaseBuilder(context, "db-app")
.apply { migrations.forEach { addMigrations(it) } }
.build()
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt
index bd1d5fa6..c4371a3e 100644
--- a/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/di/AppDatabaseModule.kt
@@ -26,154 +26,23 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.proton.android.drive.db.AppDatabase
-import me.proton.core.account.data.db.AccountDatabase
-import me.proton.core.challenge.data.db.ChallengeDatabase
-import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase
-import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase
-import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase
-import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase
-import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase
-import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase
-import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase
-import me.proton.core.drive.folder.data.db.FolderDatabase
-import me.proton.core.drive.link.data.db.LinkDatabase
-import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase
-import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase
-import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase
-import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase
-import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase
-import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase
-import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase
-import me.proton.core.drive.notification.data.db.NotificationDatabase
-import me.proton.core.drive.share.data.db.ShareDatabase
-import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase
-import me.proton.core.drive.sorting.data.db.SortingDatabase
-import me.proton.core.drive.volume.data.db.VolumeDatabase
-import me.proton.core.eventmanager.data.db.EventMetadataDatabase
-import me.proton.core.featureflag.data.db.FeatureFlagDatabase
-import me.proton.core.humanverification.data.db.HumanVerificationDatabase
-import me.proton.core.key.data.db.KeySaltDatabase
-import me.proton.core.key.data.db.PublicAddressDatabase
-import me.proton.core.payment.data.local.db.PaymentDatabase
-import me.proton.core.user.data.db.AddressDatabase
-import me.proton.core.user.data.db.UserDatabase
-import me.proton.core.usersettings.data.db.OrganizationDatabase
-import me.proton.core.usersettings.data.db.UserSettingsDatabase
-import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase
+import me.proton.android.drive.lock.data.db.AppLockDatabase
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppDatabaseModule {
+
@Provides
@Singleton
fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase =
AppDatabase.buildDatabase(context)
-}
+}
@Module
@InstallIn(SingletonComponent::class)
abstract class AppDatabaseBindsModule {
@Binds
- abstract fun provideVolumeDatabase(db: AppDatabase): VolumeDatabase
-
- @Binds
- abstract fun provideShareDatabase(db: AppDatabase): ShareDatabase
-
- @Binds
- abstract fun provideShareUrlDatabase(db: AppDatabase): ShareUrlDatabase
-
- @Binds
- abstract fun provideLinkDatabase(db: AppDatabase): LinkDatabase
-
- @Binds
- abstract fun provideFolderDatabase(db: AppDatabase): FolderDatabase
-
- @Binds
- abstract fun provideLinkAncestorDatabase(db: AppDatabase): LinkAncestorDatabase
-
- @Binds
- abstract fun provideLinkOfflineDatabase(db: AppDatabase): LinkOfflineDatabase
-
- @Binds
- abstract fun provideLinkDownloadDatabase(db: AppDatabase): LinkDownloadDatabase
-
- @Binds
- abstract fun provideLinkTrashDatabase(db: AppDatabase): LinkTrashDatabase
-
- @Binds
- abstract fun provideLinkSelectionDatabase(db: AppDatabase): LinkSelectionDatabase
-
- @Binds
- abstract fun provideMessageQueueDatabase(db: AppDatabase): MessageQueueDatabase
-
- @Binds
- abstract fun provideSortingDatabase(db: AppDatabase): SortingDatabase
-
- @Binds
- abstract fun provideLinkUploadDatabase(db: AppDatabase): LinkUploadDatabase
-
- @Binds
- abstract fun provideAccountDatabase(db: AppDatabase): AccountDatabase
-
- @Binds
- abstract fun provideUserDatabase(db: AppDatabase): UserDatabase
-
- @Binds
- abstract fun provideAddressDatabase(db: AppDatabase): AddressDatabase
-
- @Binds
- abstract fun provideFeatureFlagDatabase(db: AppDatabase): FeatureFlagDatabase
-
- @Binds
- abstract fun provideKeySaltDatabase(db: AppDatabase): KeySaltDatabase
-
- @Binds
- abstract fun providePublicAddressDatabase(db: AppDatabase): PublicAddressDatabase
-
- @Binds
- abstract fun provideHumanVerificationDatabase(db: AppDatabase): HumanVerificationDatabase
-
- @Binds
- abstract fun provideUserSettingsDatabase(db: AppDatabase): UserSettingsDatabase
-
- @Binds
- abstract fun provideOrganizationDatabase(db: AppDatabase): OrganizationDatabase
-
- @Binds
- abstract fun provideAppUiSettingsDatabase(db: AppDatabase): AppUiSettingsDatabase
-
- @Binds
- abstract fun provideEventMetadataDatabase(db: AppDatabase): EventMetadataDatabase
-
- @Binds
- abstract fun provideChallengeDatabase(appDatabase: AppDatabase): ChallengeDatabase
-
- @Binds
- abstract fun provideDriveLinkDatabase(db: AppDatabase): DriveLinkDatabase
-
- @Binds
- abstract fun provideDriveLinkPagedDatabase(db: AppDatabase): DriveLinkPagedDatabase
-
- @Binds
- abstract fun provideDriveLinkTrashDatabase(db: AppDatabase): DriveLinkTrashDatabase
-
- @Binds
- abstract fun provideDriveLinkOfflineDatabase(db: AppDatabase): DriveLinkOfflineDatabase
-
- @Binds
- abstract fun provideDriveLinkDownloadDatabase(db: AppDatabase): DriveLinkDownloadDatabase
-
- @Binds
- abstract fun provideDriveLinkSharedDatabase(db: AppDatabase): DriveLinkSharedDatabase
-
- @Binds
- abstract fun provideDriveLinkSelectionDatabase(db: AppDatabase): DriveLinkSelectionDatabase
-
- @Binds
- abstract fun provideNotificationDatabase(db: AppDatabase): NotificationDatabase
-
- @Binds
- abstract fun providePaymentDatabase(db: AppDatabase): PaymentDatabase
+ abstract fun provideAppLockDatabase(db: AppDatabase): AppLockDatabase
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt
index 3f2b2674..a21aa4dd 100644
--- a/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/di/ApplicationModule.kt
@@ -18,6 +18,7 @@
package me.proton.android.drive.di
+import android.app.ActivityManager
import android.content.ClipboardManager
import android.content.Context
import androidx.core.app.NotificationManagerCompat
@@ -29,15 +30,19 @@ import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import me.proton.android.drive.BuildConfig
+import me.proton.android.drive.lock.data.usecase.BuildAppKeyImpl
+import me.proton.android.drive.lock.domain.usecase.BuildAppKey
import me.proton.android.drive.log.DriveLogger
import me.proton.android.drive.notification.AppNotificationBuilderProvider
import me.proton.android.drive.notification.AppNotificationEventHandler
import me.proton.android.drive.provider.BuildConfigurationProvider
import me.proton.android.drive.settings.DebugSettings
+import me.proton.android.drive.usecase.GetDocumentsProviderRootsImpl
import me.proton.core.account.domain.entity.AccountType
import me.proton.core.domain.entity.AppStore
import me.proton.core.domain.entity.Product
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentsProviderRoots
import me.proton.core.drive.notification.data.provider.NotificationBuilderProvider
import me.proton.core.drive.notification.domain.handler.NotificationEventHandler
import me.proton.drive.android.settings.data.datastore.AppUiSettingsDataStore
@@ -79,7 +84,7 @@ object ApplicationModule {
@Provides
@Singleton
- fun provideDriveLogger(): DriveLogger = DriveLogger()
+ fun provideDriveLogger(@ApplicationContext context: Context): DriveLogger = DriveLogger(context)
@Provides
@Singleton
@@ -105,6 +110,11 @@ object ApplicationModule {
@Singleton
fun provideClipboardManager(@ApplicationContext context: Context): ClipboardManager =
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+
+ @Provides
+ @Singleton
+ fun provideActivityManager(@ApplicationContext context: Context): ActivityManager =
+ context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
}
@Module
@@ -119,4 +129,8 @@ abstract class ApplicationBindsModule {
abstract fun bindsNotificationBuilderProvider(
impl: AppNotificationBuilderProvider
): NotificationBuilderProvider
+
+ @Binds
+ @Singleton
+ abstract fun bindsGetDocumentsProviderRootsImpl(impl: GetDocumentsProviderRootsImpl): GetDocumentsProviderRoots
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt b/app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt
new file mode 100644
index 00000000..c306c64d
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/di/DriveDatabaseModule.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.di
+
+import android.content.Context
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import me.proton.android.drive.db.DriveDatabase
+import me.proton.core.account.data.db.AccountDatabase
+import me.proton.core.challenge.data.db.ChallengeDatabase
+import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase
+import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase
+import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase
+import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase
+import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase
+import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase
+import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase
+import me.proton.core.drive.folder.data.db.FolderDatabase
+import me.proton.core.drive.link.data.db.LinkDatabase
+import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase
+import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase
+import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase
+import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase
+import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase
+import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase
+import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase
+import me.proton.core.drive.notification.data.db.NotificationDatabase
+import me.proton.core.drive.share.data.db.ShareDatabase
+import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase
+import me.proton.core.drive.sorting.data.db.SortingDatabase
+import me.proton.core.drive.volume.data.db.VolumeDatabase
+import me.proton.core.eventmanager.data.db.EventMetadataDatabase
+import me.proton.core.featureflag.data.db.FeatureFlagDatabase
+import me.proton.core.humanverification.data.db.HumanVerificationDatabase
+import me.proton.core.key.data.db.KeySaltDatabase
+import me.proton.core.key.data.db.PublicAddressDatabase
+import me.proton.core.observability.data.db.ObservabilityDatabase
+import me.proton.core.payment.data.local.db.PaymentDatabase
+import me.proton.core.user.data.db.AddressDatabase
+import me.proton.core.user.data.db.UserDatabase
+import me.proton.core.usersettings.data.db.OrganizationDatabase
+import me.proton.core.usersettings.data.db.UserSettingsDatabase
+import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object DriveDatabaseModule {
+ @Provides
+ @Singleton
+ fun provideDriveDatabase(@ApplicationContext context: Context): DriveDatabase =
+ DriveDatabase.buildDatabase(context)
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+abstract class DriveDatabaseBindsModule {
+
+ @Binds
+ abstract fun provideVolumeDatabase(db: DriveDatabase): VolumeDatabase
+
+ @Binds
+ abstract fun provideShareDatabase(db: DriveDatabase): ShareDatabase
+
+ @Binds
+ abstract fun provideShareUrlDatabase(db: DriveDatabase): ShareUrlDatabase
+
+ @Binds
+ abstract fun provideLinkDatabase(db: DriveDatabase): LinkDatabase
+
+ @Binds
+ abstract fun provideFolderDatabase(db: DriveDatabase): FolderDatabase
+
+ @Binds
+ abstract fun provideLinkAncestorDatabase(db: DriveDatabase): LinkAncestorDatabase
+
+ @Binds
+ abstract fun provideLinkOfflineDatabase(db: DriveDatabase): LinkOfflineDatabase
+
+ @Binds
+ abstract fun provideLinkDownloadDatabase(db: DriveDatabase): LinkDownloadDatabase
+
+ @Binds
+ abstract fun provideLinkTrashDatabase(db: DriveDatabase): LinkTrashDatabase
+
+ @Binds
+ abstract fun provideLinkSelectionDatabase(db: DriveDatabase): LinkSelectionDatabase
+
+ @Binds
+ abstract fun provideMessageQueueDatabase(db: DriveDatabase): MessageQueueDatabase
+
+ @Binds
+ abstract fun provideSortingDatabase(db: DriveDatabase): SortingDatabase
+
+ @Binds
+ abstract fun provideLinkUploadDatabase(db: DriveDatabase): LinkUploadDatabase
+
+ @Binds
+ abstract fun provideAccountDatabase(db: DriveDatabase): AccountDatabase
+
+ @Binds
+ abstract fun provideUserDatabase(db: DriveDatabase): UserDatabase
+
+ @Binds
+ abstract fun provideAddressDatabase(db: DriveDatabase): AddressDatabase
+
+ @Binds
+ abstract fun provideFeatureFlagDatabase(db: DriveDatabase): FeatureFlagDatabase
+
+ @Binds
+ abstract fun provideKeySaltDatabase(db: DriveDatabase): KeySaltDatabase
+
+ @Binds
+ abstract fun providePublicAddressDatabase(db: DriveDatabase): PublicAddressDatabase
+
+ @Binds
+ abstract fun provideHumanVerificationDatabase(db: DriveDatabase): HumanVerificationDatabase
+
+ @Binds
+ abstract fun provideUserSettingsDatabase(db: DriveDatabase): UserSettingsDatabase
+
+ @Binds
+ abstract fun provideOrganizationDatabase(db: DriveDatabase): OrganizationDatabase
+
+ @Binds
+ abstract fun provideAppUiSettingsDatabase(db: DriveDatabase): AppUiSettingsDatabase
+
+ @Binds
+ abstract fun provideEventMetadataDatabase(db: DriveDatabase): EventMetadataDatabase
+
+ @Binds
+ abstract fun provideChallengeDatabase(driveDatabase: DriveDatabase): ChallengeDatabase
+
+ @Binds
+ abstract fun provideDriveLinkDatabase(db: DriveDatabase): DriveLinkDatabase
+
+ @Binds
+ abstract fun provideDriveLinkPagedDatabase(db: DriveDatabase): DriveLinkPagedDatabase
+
+ @Binds
+ abstract fun provideDriveLinkTrashDatabase(db: DriveDatabase): DriveLinkTrashDatabase
+
+ @Binds
+ abstract fun provideDriveLinkOfflineDatabase(db: DriveDatabase): DriveLinkOfflineDatabase
+
+ @Binds
+ abstract fun provideDriveLinkDownloadDatabase(db: DriveDatabase): DriveLinkDownloadDatabase
+
+ @Binds
+ abstract fun provideDriveLinkSharedDatabase(db: DriveDatabase): DriveLinkSharedDatabase
+
+ @Binds
+ abstract fun provideDriveLinkSelectionDatabase(db: DriveDatabase): DriveLinkSelectionDatabase
+
+ @Binds
+ abstract fun provideNotificationDatabase(db: DriveDatabase): NotificationDatabase
+
+ @Binds
+ abstract fun providePaymentDatabase(db: DriveDatabase): PaymentDatabase
+
+ @Binds
+ abstract fun provideObservabilityDatabase(db: DriveDatabase): ObservabilityDatabase
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt b/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt
index df5b91a4..7ade3ddb 100644
--- a/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/extension/DriveException.kt
@@ -19,18 +19,19 @@
package me.proton.android.drive.extension
import android.content.Context
+import me.proton.android.drive.lock.domain.exception.LockException
import me.proton.core.drive.base.domain.exception.DriveException
import me.proton.core.drive.base.presentation.R as BasePresentation
import me.proton.core.drive.share.domain.exception.ShareException
import me.proton.core.util.kotlin.CoreLogger
+import me.proton.android.drive.lock.presentation.extension.getDefaultMessage as lockGetDefaultMessage
-fun DriveException.getDefaultMessage(context: Context): String = context.getString(
- when (this) {
- is ShareException.MainShareLocked -> BasePresentation.string.error_main_share_locked
- is ShareException.MainShareNotFound -> BasePresentation.string.error_main_share_not_found
- else -> throw IllegalStateException("Default message for exception is missing")
- }
-)
+fun DriveException.getDefaultMessage(context: Context): String = when (this) {
+ is ShareException.MainShareLocked -> context.getString(BasePresentation.string.error_main_share_locked)
+ is ShareException.MainShareNotFound -> context.getString(BasePresentation.string.error_main_share_not_found)
+ is LockException -> lockGetDefaultMessage(context)
+ else -> throw IllegalStateException("Default message for exception is missing")
+}
fun DriveException.log(tag: String, message: String = this.message.orEmpty()): DriveException = also {
CoreLogger.d(tag, this, message)
diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt
new file mode 100644
index 00000000..1645acbe
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/initializer/AutoLockInitializer.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+package me.proton.android.drive.initializer
+
+import android.content.Context
+import androidx.lifecycle.coroutineScope
+import androidx.startup.Initializer
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.android.EntryPointAccessors
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.android.drive.lock.domain.manager.AutoLockManager
+import me.proton.core.presentation.app.AppLifecycleProvider
+
+class AutoLockInitializer : Initializer {
+
+ override fun create(context: Context) {
+ with (
+ EntryPointAccessors.fromApplication(
+ context.applicationContext,
+ AutoLockInitializerEntryPoint::class.java
+ )
+ ) {
+ appLifecycleProvider.state
+ .onEach { state ->
+ if (appLockManager.isEnabled()) {
+ when (state) {
+ AppLifecycleProvider.State.Background -> autoLockManager.autoLock()
+ AppLifecycleProvider.State.Foreground -> autoLockManager.cancelAutoLock()
+ }
+ }
+ }
+ .launchIn(appLifecycleProvider.lifecycle.coroutineScope)
+ }
+ }
+
+ override fun dependencies(): List>> = listOf(
+ LoggerInitializer::class.java,
+ WorkManagerInitializer::class.java,
+ )
+
+ @EntryPoint
+ @InstallIn(SingletonComponent::class)
+ interface AutoLockInitializerEntryPoint {
+ val appLifecycleProvider: AppLifecycleProvider
+ val appLockManager: AppLockManager
+ val autoLockManager: AutoLockManager
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt
new file mode 100644
index 00000000..a3ed2cd5
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/initializer/DocumentsProviderInitializer.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.initializer
+
+import android.content.Context
+import androidx.lifecycle.coroutineScope
+import androidx.startup.Initializer
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.android.EntryPointAccessors
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.core.drive.documentsprovider.data.DriveDocumentsProvider
+import me.proton.core.presentation.app.AppLifecycleProvider
+
+class DocumentsProviderInitializer : Initializer {
+
+ override fun create(context: Context) {
+ with (
+ EntryPointAccessors.fromApplication(
+ context.applicationContext,
+ AutoLockInitializerEntryPoint::class.java
+ )
+ ) {
+ appLockManager.enabled
+ .onEach {
+ DriveDocumentsProvider.notifyRootsHaveChanged(context)
+ }
+ .launchIn(appLifecycleProvider.lifecycle.coroutineScope)
+ }
+ }
+
+ override fun dependencies(): List>> = listOf(
+ LoggerInitializer::class.java,
+ )
+
+ @EntryPoint
+ @InstallIn(SingletonComponent::class)
+ interface AutoLockInitializerEntryPoint {
+ val appLifecycleProvider: AppLifecycleProvider
+ val appLockManager: AppLockManager
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt b/app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt
new file mode 100644
index 00000000..5b1581d3
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/initializer/MainInitializer.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.initializer
+
+import android.content.Context
+import androidx.startup.AppInitializer
+import androidx.startup.Initializer
+import me.proton.core.auth.presentation.MissingScopeInitializer
+import me.proton.core.humanverification.presentation.HumanVerificationInitializer
+import me.proton.core.network.presentation.init.UnAuthSessionFetcherInitializer
+import me.proton.core.plan.presentation.UnredeemedPurchaseInitializer
+
+class MainInitializer : Initializer {
+
+ override fun create(context: Context) {
+ // No-op needed
+ }
+
+ override fun dependencies() = listOf(
+ EventManagerInitializer::class.java,
+ HumanVerificationInitializer::class.java,
+ UnredeemedPurchaseInitializer::class.java,
+ MissingScopeInitializer::class.java,
+ UnAuthSessionFetcherInitializer::class.java,
+ AutoLockInitializer::class.java,
+ )
+
+ companion object {
+
+ fun init(appContext: Context) {
+ with(AppInitializer.getInstance(appContext)) {
+ // WorkManager need to be initialized before any other dependant initializer.
+ initializeComponent(WorkManagerInitializer::class.java)
+ initializeComponent(MainInitializer::class.java)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt b/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt
index a07bec08..b5483c59 100644
--- a/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/log/DriveLogger.kt
@@ -20,21 +20,28 @@
package me.proton.android.drive.log
+import android.content.Context
import android.os.Build
import android.os.LocaleList
+import dagger.hilt.android.qualifiers.ApplicationContext
import io.sentry.Breadcrumb
import io.sentry.Sentry
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.protocol.Message
+import me.proton.core.drive.base.presentation.extension.getDefaultMessage
import me.proton.core.util.kotlin.Logger
import me.proton.core.util.kotlin.LoggerLogTag
import timber.log.Timber
import java.util.Locale
+import javax.inject.Inject
import javax.inject.Singleton
+import me.proton.core.drive.base.presentation.R as BasePresentation
@Singleton
-class DriveLogger : Logger {
+class DriveLogger @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+) : Logger {
override fun v(tag: String, message: String) {
DriveSentry.addBreadcrumb(tag, message)
@@ -61,11 +68,11 @@ class DriveLogger : Logger {
Timber.tag(tag).i(e, message)
}
override fun e(tag: String, e: Throwable) {
- DriveSentry.captureException(tag, e)
+ DriveSentry.captureException(appContext, tag, e)
Timber.tag(tag).e(e)
}
override fun e(tag: String, e: Throwable, message: String) {
- DriveSentry.captureException(tag, e, message)
+ DriveSentry.captureException(appContext, tag, e, message)
Timber.tag(tag).e(e, message)
}
override fun log(tag: LoggerLogTag, message: String) = i(tag.name, message)
@@ -85,12 +92,24 @@ class DriveLogger : Logger {
private object DriveSentry {
- fun captureException(tag: String, e: Throwable) {
+ fun captureException(
+ context: Context,
+ tag: String,
+ e: Throwable,
+ ) {
+ setInternalErrorTag(context, e)
Sentry.setTag("CoreLogger", tag)
Sentry.captureException(e)
}
- fun captureException(tag: String, e: Throwable, message: String, level: SentryLevel = SentryLevel.ERROR) {
+ fun captureException(
+ context: Context,
+ tag: String,
+ e: Throwable,
+ message: String,
+ level: SentryLevel = SentryLevel.ERROR,
+ ) {
+ setInternalErrorTag(context, e)
Sentry.setTag("CoreLogger", tag)
Sentry.captureEvent(
SentryEvent(e).apply {
@@ -122,6 +141,15 @@ class DriveLogger : Logger {
}
)
}
+
+ private fun setInternalErrorTag(context: Context, e: Throwable) {
+ val internalErrorMessage = context.getString(BasePresentation.string.common_error_internal)
+ val errorMessage = e.getDefaultMessage(
+ context = context,
+ useExceptionMessage = false,
+ )
+ Sentry.setTag("InternalError", (errorMessage == internalErrorMessage).toString())
+ }
}
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt b/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt
index 0567f7b4..c61ca54c 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/MainActivity.kt
@@ -69,6 +69,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import me.proton.android.drive.extension.deepLinkBaseUrl
+import me.proton.android.drive.lock.data.provider.BiometricPromptProvider
+import me.proton.android.drive.lock.domain.manager.AppLockManager
import me.proton.android.drive.log.DriveLogTag
import me.proton.android.drive.ui.navigation.AppNavGraph
import me.proton.android.drive.ui.provider.LocalSnackbarPadding
@@ -100,6 +102,8 @@ class MainActivity : FragmentActivity() {
@Inject lateinit var actionProvider: ActionProvider
@Inject lateinit var getThemeStyle: GetThemeStyle
@Inject lateinit var processIntent: ProcessIntent
+ @Inject lateinit var biometricPromptProvider: BiometricPromptProvider
+ @Inject lateinit var appLockManager: AppLockManager
lateinit var configurationProvider: ConfigurationProvider
private val accountViewModel: AccountViewModel by viewModels()
private val bugReportViewModel: BugReportViewModel by viewModels()
@@ -121,6 +125,7 @@ class MainActivity : FragmentActivity() {
applySecureFlag()
setTheme(CorePresentation.style.ProtonTheme_Drive)
super.onCreate(savedInstanceState)
+ biometricPromptProvider.bindToActivity(this)
setupAccountsViewModel()
bugReportViewModel.initialize(this)
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -145,12 +150,13 @@ class MainActivity : FragmentActivity() {
deepLinkBaseUrl = this@MainActivity.deepLinkBaseUrl,
clearBackstackTrigger = clearBackstackTrigger,
deepLinkIntent = deepLinkIntent,
+ locked = appLockManager.locked,
+ primaryAccount = accountViewModel.primaryAccount,
exitApp = { finish() },
sendBugReport = bugReportViewModel::sendBugReport,
) { isOpen ->
isDrawerOpen = isOpen
}
-
HeadlessSnackBar(snackbarHostState)
}
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt
new file mode 100644
index 00000000..6331daf8
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/AutoLockDurations.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.dialog
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import me.proton.android.drive.ui.viewevent.AutoLockDurationsViewEvent
+import me.proton.android.drive.ui.viewmodel.AutoLockDurationsViewModel
+import me.proton.android.drive.ui.viewstate.AutoLockDurationsViewState
+import me.proton.core.compose.component.bottomsheet.BottomSheetContent
+import me.proton.core.compose.component.bottomsheet.RunAction
+import me.proton.core.compose.flow.rememberFlowWithLifecycle
+import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing
+import me.proton.core.drive.settings.presentation.extension.toString
+
+@Composable
+fun AutoLockDurations(
+ runAction: RunAction,
+ dismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val viewModel = hiltViewModel()
+ val viewState by rememberFlowWithLifecycle(flow = viewModel.viewState)
+ .collectAsState(initial = viewModel.initialViewState)
+ AutoLockDurations(
+ viewState = viewState,
+ viewEvent = viewModel.viewEvent(runAction, dismiss),
+ modifier = modifier.navigationBarsPadding(),
+ )
+}
+
+@Composable
+fun AutoLockDurations(
+ viewState: AutoLockDurationsViewState,
+ viewEvent: AutoLockDurationsViewEvent,
+ modifier: Modifier = Modifier,
+) {
+ BottomSheetContent(
+ modifier = modifier,
+ header = {
+ Text(text = viewState.title)
+ },
+ content = {
+ viewState.durations.forEach { duration ->
+ Duration(
+ title = duration.toString(LocalContext.current),
+ isSelected = duration == viewState.selected,
+ ) {
+ viewEvent.onDuration(duration)
+ }
+ }
+ }
+ )
+}
+
+@Composable
+private fun Duration(
+ title: String,
+ isSelected: Boolean,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable { onClick() },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = title,
+ modifier = Modifier
+ .padding(start = DefaultSpacing)
+ .weight(1f),
+ )
+ RadioButton(
+ selected = isSelected,
+ onClick = { onClick() },
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun PreviewSelectedDuration() {
+ Duration(title = "Immediately", isSelected = true) {}
+}
+
+@Preview
+@Composable
+private fun PreviewUnselectedDuration() {
+ Duration(title = "15 minutes", isSelected = false) {}
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt
new file mode 100644
index 00000000..71049641
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/dialog/SystemAccessDialog.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.dialog
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.hilt.navigation.compose.hiltViewModel
+import me.proton.android.drive.ui.viewmodel.SystemAccessDialogViewModel
+import me.proton.core.compose.component.ProtonAlertDialog
+import me.proton.core.compose.component.ProtonAlertDialogButton
+import me.proton.core.compose.component.ProtonAlertDialogText
+import me.proton.core.drive.base.presentation.R
+
+@Composable
+fun SystemAccessDialog(
+ modifier: Modifier = Modifier,
+ onDismiss: () -> Unit,
+) {
+ val viewModel = hiltViewModel()
+ SystemAccessDialog(
+ modifier = modifier,
+ onDismiss = onDismiss,
+ onSettings = viewModel.viewEvent(LocalContext.current, onDismiss).onSettings,
+ )
+}
+
+@Composable
+fun SystemAccessDialog(
+ modifier: Modifier = Modifier,
+ onDismiss: () -> Unit,
+ onSettings: () -> Unit,
+) {
+ ProtonAlertDialog(
+ modifier = modifier,
+ titleResId = R.string.app_lock_system_dialog_title,
+ text = {
+ Column {
+ ProtonAlertDialogText(textResId = R.string.app_lock_system_dialog_description)
+ }
+ },
+ onDismissRequest = onDismiss,
+ dismissButton = {
+ ProtonAlertDialogButton(
+ titleResId = R.string.common_cancel_action,
+ onClick = onDismiss,
+ )
+ },
+ confirmButton = {
+ ProtonAlertDialogButton(
+ titleResId = R.string.app_lock_system_dialog_settings_button,
+ onClick = onSettings,
+ )
+ }
+ )
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt
index 682dc91b..ea300a15 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/AppNavGraph.kt
@@ -43,6 +43,7 @@ import androidx.navigation.navArgument
import androidx.navigation.navDeepLink
import com.google.accompanist.navigation.animation.composable
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.collectLatest
import me.proton.android.drive.extension.get
@@ -50,7 +51,9 @@ import me.proton.android.drive.extension.popAllBackStack
import me.proton.android.drive.extension.require
import me.proton.android.drive.extension.requireArguments
import me.proton.android.drive.extension.runFromRoute
+import me.proton.android.drive.lock.presentation.component.AppLock
import me.proton.android.drive.log.DriveLogTag
+import me.proton.android.drive.ui.dialog.AutoLockDurations
import me.proton.android.drive.ui.dialog.ConfirmDeletionDialog
import me.proton.android.drive.ui.dialog.ConfirmEmptyTrashDialog
import me.proton.android.drive.ui.dialog.ConfirmStopSharingDialog
@@ -59,6 +62,7 @@ import me.proton.android.drive.ui.dialog.MultipleFileOrFolderOptions
import me.proton.android.drive.ui.dialog.ParentFolderOptions
import me.proton.android.drive.ui.dialog.SendFileDialog
import me.proton.android.drive.ui.dialog.SortingList
+import me.proton.android.drive.ui.dialog.SystemAccessDialog
import me.proton.android.drive.ui.navigation.animation.defaultEnterSlideTransition
import me.proton.android.drive.ui.navigation.animation.defaultPopExitSlideTransition
import me.proton.android.drive.ui.navigation.animation.slideComposable
@@ -67,6 +71,7 @@ import me.proton.android.drive.ui.navigation.internal.MutableNavControllerSaver
import me.proton.android.drive.ui.navigation.internal.createNavController
import me.proton.android.drive.ui.navigation.internal.modalBottomSheet
import me.proton.android.drive.ui.navigation.internal.rememberAnimatedNavController
+import me.proton.android.drive.ui.screen.AppAccessScreen
import me.proton.android.drive.ui.screen.FileInfoScreen
import me.proton.android.drive.ui.screen.HomeScreen
import me.proton.android.drive.ui.screen.LauncherScreen
@@ -78,6 +83,7 @@ import me.proton.android.drive.ui.screen.SigningOutScreen
import me.proton.android.drive.ui.screen.TrashScreen
import me.proton.android.drive.ui.screen.UploadToScreen
import me.proton.android.drive.ui.screen.WelcomeScreen
+import me.proton.core.account.domain.entity.Account
import me.proton.core.compose.component.bottomsheet.ModalBottomSheetViewState
import me.proton.core.crypto.common.keystore.KeyStoreCrypto
import me.proton.core.domain.entity.UserId
@@ -101,6 +107,8 @@ fun AppNavGraph(
deepLinkBaseUrl: String,
clearBackstackTrigger: SharedFlow,
deepLinkIntent: SharedFlow,
+ locked: Flow,
+ primaryAccount: Flow,
exitApp: () -> Unit,
sendBugReport: () -> Unit,
onDrawerStateChanged: (Boolean) -> Unit,
@@ -136,14 +144,16 @@ fun AppNavGraph(
homeNavController = createNavController(localContext)
}
}
- AppNavGraph(
- navController = navController,
- homeNavController = homeNavController,
- deepLinkBaseUrl = deepLinkBaseUrl,
- exitApp = exitApp,
- sendBugReport = sendBugReport,
- onDrawerStateChanged = onDrawerStateChanged,
- )
+ AppLock(locked = locked, primaryAccount = primaryAccount) {
+ AppNavGraph(
+ navController = navController,
+ homeNavController = homeNavController,
+ deepLinkBaseUrl = deepLinkBaseUrl,
+ exitApp = exitApp,
+ sendBugReport = sendBugReport,
+ onDrawerStateChanged = onDrawerStateChanged,
+ )
+ }
}
@Composable
@@ -188,6 +198,9 @@ fun AppNavGraph(
addShareViaLink(navController)
addDiscardShareViaLinkChanges(navController)
addUploadTo(navController, deepLinkBaseUrl, exitApp)
+ addAppAccess(navController)
+ addSystemAccessDialog(navController)
+ addAutoLockDurations(navController)
}
}
@@ -673,9 +686,16 @@ fun NavGraphBuilder.addSettings(navController: NavHostController) = composable(
arguments = listOf(
navArgument(Screen.PagerPreview.USER_ID) { type = NavType.StringType },
),
-) {
+) { navBackStackEntry ->
+ val userId = UserId(navBackStackEntry.require(Screen.Files.USER_ID))
SettingsScreen(
navigateBack = { navController.popBackStack() },
+ navigateToAppAccess = {
+ navController.navigate(Screen.Settings.AppAccess(userId))
+ },
+ navigateToAutoLockDurations = {
+ navController.navigate(Screen.Settings.AutoLockDurations(userId))
+ },
)
}
@@ -872,3 +892,47 @@ fun NavGraphBuilder.addUploadTo(
exitApp = exitApp,
)
}
+
+@ExperimentalAnimationApi
+fun NavGraphBuilder.addAppAccess(navController: NavHostController) = composable(
+ route = Screen.Settings.AppAccess.route,
+ enterTransition = defaultEnterSlideTransition { true },
+ exitTransition = { ExitTransition.None },
+ popEnterTransition = { EnterTransition.None },
+ popExitTransition = defaultPopExitSlideTransition { true },
+ arguments = listOf(
+ navArgument(Screen.Settings.USER_ID) { type = NavType.StringType },
+ ),
+) { navBackStackEntry ->
+ val userId = UserId(navBackStackEntry.require(Screen.Settings.USER_ID))
+ AppAccessScreen(
+ navigateToSystemAccess = {
+ navController.navigate(Screen.Settings.AppAccess.Dialogs.SystemAccess(userId))
+ },
+ navigateBack = {
+ navController.popBackStack()
+ },
+ )
+}
+
+@ExperimentalCoroutinesApi
+fun NavGraphBuilder.addSystemAccessDialog(navController: NavHostController) = dialog(
+ route = Screen.Settings.AppAccess.Dialogs.SystemAccess.route,
+ arguments = listOf(
+ navArgument(Screen.Settings.USER_ID) { type = NavType.StringType },
+ ),
+) {
+ SystemAccessDialog(onDismiss = { navController.popBackStack() })
+}
+
+fun NavGraphBuilder.addAutoLockDurations(
+ navController: NavHostController,
+) = modalBottomSheet(
+ route = Screen.Settings.AutoLockDurations.route,
+ viewState = ModalBottomSheetViewState(dismissOnAction = false),
+) { _, runAction ->
+ AutoLockDurations(
+ runAction = runAction,
+ dismiss = { navController.popBackStack() }
+ )
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt
index f6cf989c..a948b678 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/navigation/Screen.kt
@@ -271,6 +271,22 @@ sealed class Screen(val route: String) {
operator fun invoke(userId: UserId) = "settings/${userId.id}"
const val USER_ID = Screen.USER_ID
+
+ object AppAccess : Screen("settings/{userId}/appAccess") {
+
+ operator fun invoke(userId: UserId) = "settings/${userId.id}/appAccess"
+
+ object Dialogs {
+
+ object SystemAccess : Screen("settings/{userId}/appAccess/systemAccess") {
+ operator fun invoke(userId: UserId) = "settings/${userId.id}/appAccess/systemAccess"
+ }
+ }
+ }
+
+ object AutoLockDurations : Screen("settings/{userId}/autoLockDurations") {
+ operator fun invoke(userId: UserId) = "settings/${userId.id}/autoLockDurations"
+ }
}
object SendFile : Screen("send/{userId}/shares/{shareId}/files/{fileId}") {
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt
new file mode 100644
index 00000000..5f47a6b1
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/AppAccessScreen.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.screen
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.RadioButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.hilt.navigation.compose.hiltViewModel
+import me.proton.android.drive.ui.viewevent.AppAccessViewEvent
+import me.proton.android.drive.ui.viewmodel.AppAccessViewModel
+import me.proton.android.drive.ui.viewstate.AccessOption
+import me.proton.android.drive.ui.viewstate.AppAccessViewState
+import me.proton.core.compose.flow.rememberFlowWithLifecycle
+import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing
+import me.proton.core.compose.theme.ProtonTheme
+import me.proton.core.drive.base.presentation.component.ProtonListItem
+import me.proton.core.drive.base.presentation.component.TopAppBar
+import me.proton.core.drive.base.presentation.R as BasePresentation
+import me.proton.core.presentation.R as CorePresentation
+
+@Composable
+fun AppAccessScreen(
+ modifier: Modifier = Modifier,
+ navigateToSystemAccess: () -> Unit,
+ navigateBack: () -> Unit,
+) {
+ val viewModel = hiltViewModel()
+ val viewState by rememberFlowWithLifecycle(flow = viewModel.viewState)
+ .collectAsState(initial = viewModel.initialViewState)
+ AppAccess(
+ viewState = viewState,
+ viewEvent = viewModel.viewEvent(navigateToSystemAccess, navigateBack),
+ modifier = modifier.fillMaxSize(),
+ navigateBack = navigateBack,
+ )
+}
+
+@Composable
+fun AppAccess(
+ viewState: AppAccessViewState,
+ viewEvent: AppAccessViewEvent,
+ modifier: Modifier = Modifier,
+ navigateBack: () -> Unit,
+) {
+ Column(modifier = modifier) {
+ TopAppBar(
+ navigationIcon = painterResource(id = CorePresentation.drawable.ic_arrow_back),
+ onNavigationIcon = navigateBack,
+ title = viewState.title,
+ modifier = Modifier.statusBarsPadding()
+ )
+ AppAccessOptions(viewState.enabledOption, viewEvent)
+ }
+}
+
+@Composable
+fun AppAccessOptions(
+ enabledOption: AccessOption,
+ viewEvent: AppAccessViewEvent,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.verticalScroll(rememberScrollState())
+ ) {
+ AppAccessOption(
+ iconResId = BasePresentation.drawable.ic_proton_lock_open,
+ titleResId = BasePresentation.string.app_lock_option_none,
+ isSelected = enabledOption == AccessOption.NONE,
+ ) {
+ viewEvent.onDisable()
+ }
+ AppAccessOption(
+ iconResId = CorePresentation.drawable.ic_proton_fingerprint,
+ titleResId = BasePresentation.string.app_lock_option_system,
+ isSelected = enabledOption == AccessOption.SYSTEM,
+ ) {
+ viewEvent.onSystem()
+ }
+ }
+}
+
+@Composable
+fun AppAccessOption(
+ iconResId: Int,
+ titleResId: Int,
+ isSelected: Boolean,
+ modifier: Modifier = Modifier,
+ onClick: () -> Unit,
+) {
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable {
+ onClick()
+ },
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ ProtonListItem(
+ icon = painterResource(id = iconResId),
+ title = stringResource(id = titleResId),
+ modifier = modifier
+ .weight(1f)
+ .padding(start = DefaultSpacing),
+ )
+ RadioButton(
+ selected = isSelected,
+ onClick = { onClick() },
+ )
+ }
+}
+
+@Preview
+@Composable
+private fun AppAccessPreview() {
+ ProtonTheme {
+ AppAccess(
+ viewState = AppAccessViewState(
+ title = "Title",
+ enabledOption = AccessOption.NONE,
+ ),
+ viewEvent = object : AppAccessViewEvent {
+ override val onDisable = {}
+ override val onSystem = {}
+ },
+ navigateBack = {}
+ )
+ }
+}
+
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt
index 8c9ec600..16659ddc 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/FilesScreen.kt
@@ -66,15 +66,18 @@ fun FilesScreen(
val selected by rememberFlowWithLifecycle(flow = viewState.selected)
.collectAsState(initial = null)
val inMultiselect = remember(selected) { selected?.isNotEmpty() ?: false }
- val viewEvent = viewModel.viewEvent(
- navigateToFiles,
- navigateToPreview,
- navigateToSortingDialog,
- navigateToFileOrFolderOptions,
- navigateToMultipleFileOrFolderOptions,
- navigateToParentFolderOptions,
- navigateBack,
- )
+
+ val viewEvent = remember {
+ viewModel.viewEvent(
+ navigateToFiles,
+ navigateToPreview,
+ navigateToSortingDialog,
+ navigateToFileOrFolderOptions,
+ navigateToMultipleFileOrFolderOptions,
+ navigateToParentFolderOptions,
+ navigateBack,
+ )
+ }
BackHandler(enabled = inMultiselect) { viewEvent.onBack() }
LaunchedEffect(viewState) {
homeScaffoldState.topAppBar.value = {
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt
index fe46c2bb..f0a3fac0 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/MoveToFolder.kt
@@ -98,6 +98,7 @@ fun MoveToFolder(
modifier
.systemBarsPadding()
.padding(vertical = DefaultSpacing)
+ .testTag(MoveToFolderScreenTestTag.screen)
) {
Column {
Title(
@@ -198,5 +199,5 @@ fun TitleText(
object MoveToFolderScreenTestTag {
const val screen = "move to folder screen"
- const val plusFolderButton = "plus folder button"
+ const val plusFolderButton = "move to folder plus folder button"
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt
index d249006c..f707476e 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/OfflineScreen.kt
@@ -29,12 +29,12 @@ import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import me.proton.android.drive.ui.navigation.PagerType
import me.proton.android.drive.ui.viewmodel.OfflineViewModel
+import me.proton.core.compose.flow.rememberFlowWithLifecycle
import me.proton.core.drive.files.presentation.component.DriveLinksFlow
import me.proton.core.drive.files.presentation.component.Files
import me.proton.core.drive.link.domain.entity.FileId
import me.proton.core.drive.link.domain.entity.FolderId
import me.proton.core.drive.link.domain.entity.LinkId
-import me.proton.core.compose.flow.rememberFlowWithLifecycle
import me.proton.core.drive.sorting.domain.entity.Sorting
@Composable
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt
index ae6ef7bc..2def56e4 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/PreviewScreen.kt
@@ -107,7 +107,6 @@ fun PreviewScreen(
navigateBack = navigateBack,
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
),
- zoomEffect = viewModel.zoomEffect,
modifier = modifier,
) { page ->
viewModel.onPageChanged(page)
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt b/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt
index 30bf8932..c86e595c 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/screen/SettingsScreen.kt
@@ -41,6 +41,8 @@ import me.proton.core.drive.settings.presentation.Settings
@Composable
fun SettingsScreen(
navigateBack: () -> Unit,
+ navigateToAppAccess: () -> Unit,
+ navigateToAutoLockDurations: () -> Unit,
modifier: Modifier = Modifier,
) {
val viewModel = hiltViewModel()
@@ -54,15 +56,19 @@ fun SettingsScreen(
}.launchIn(this)
}
- settingsViewState?.let { viewState ->
- Box(
- modifier = modifier
- .fillMaxSize()
- .systemBarsPadding()
- ) {
+ Box(
+ modifier = modifier
+ .fillMaxSize()
+ .systemBarsPadding()
+ ) {
+ settingsViewState?.let { viewState ->
Settings(
viewState = viewState,
- viewEvent = viewModel.viewEvent(navigateBack),
+ viewEvent = viewModel.viewEvent(
+ navigateBack = navigateBack,
+ navigateToAppAccess = navigateToAppAccess,
+ navigateToAutoLockDurations = navigateToAutoLockDurations,
+ ),
)
ProtonSnackbarHost(
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt
new file mode 100644
index 00000000..fab81f21
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AppAccessViewEvent.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewevent
+
+interface AppAccessViewEvent {
+ val onDisable: () -> Unit
+ val onSystem: () -> Unit
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt
new file mode 100644
index 00000000..0acb58cf
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/AutoLockDurationsViewEvent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewevent
+
+import kotlin.time.Duration
+
+interface AutoLockDurationsViewEvent {
+ val onDuration: (Duration) -> Unit
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt
new file mode 100644
index 00000000..70641340
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewevent/SystemAccessDialogViewEvent.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewevent
+
+interface SystemAccessDialogViewEvent {
+ val onSettings: () -> Unit
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt
new file mode 100644
index 00000000..e2333ee6
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AppAccessViewModel.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewmodel
+
+import android.content.Context
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import me.proton.android.drive.extension.getDefaultMessage
+import me.proton.android.drive.lock.data.extension.onNotAvailable
+import me.proton.android.drive.lock.data.extension.onReady
+import me.proton.android.drive.lock.data.extension.onSetupRequired
+import me.proton.android.drive.lock.domain.entity.AppLockType
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.android.drive.lock.domain.usecase.DisableAppLock
+import me.proton.android.drive.lock.domain.usecase.EnableAppLock
+import me.proton.android.drive.lock.domain.usecase.GetLockState
+import me.proton.android.drive.ui.viewevent.AppAccessViewEvent
+import me.proton.android.drive.ui.viewstate.AccessOption
+import me.proton.android.drive.ui.viewstate.AppAccessViewState
+import me.proton.core.drive.base.domain.usecase.BroadcastMessages
+import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
+import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
+import javax.inject.Inject
+import me.proton.core.drive.base.presentation.R as BasePresentation
+
+@Suppress("StaticFieldLeak")
+@HiltViewModel
+class AppAccessViewModel @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ savedStateHandle: SavedStateHandle,
+ private val disableAppLock: DisableAppLock,
+ private val enableAppLock: EnableAppLock,
+ private val getLockState: GetLockState,
+ private val broadcastMessages: BroadcastMessages,
+ private val appLockManager: AppLockManager,
+) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
+
+ val initialViewState: AppAccessViewState = AppAccessViewState(
+ title = appContext.getString(BasePresentation.string.app_lock_access_title),
+ enabledOption = AccessOption.NONE,
+ )
+ val viewState: Flow = appLockManager.enabled.map { enabled ->
+ initialViewState.copy(
+ enabledOption = if (!enabled) AccessOption.NONE else AccessOption.SYSTEM
+ )
+ }
+
+ fun viewEvent(
+ navigateToSystemAccess: () -> Unit,
+ navigateBack: () -> Unit,
+ ): AppAccessViewEvent = object : AppAccessViewEvent {
+ override val onDisable: () -> Unit = { doDisableAppLock(navigateBack) }
+ override val onSystem: () -> Unit = {
+ getLockState()
+ .onNotAvailable {
+ broadcastMessages(
+ userId = userId,
+ message = appContext.getString(BasePresentation.string.app_lock_system_not_available),
+ type = BroadcastMessage.Type.WARNING,
+ )
+ }
+ .onSetupRequired {
+ navigateToSystemAccess()
+ }
+ .onReady {
+ doEnableSystemLock(navigateBack)
+ }
+ }
+ }
+
+ private fun doDisableAppLock(navigateBack: () -> Unit) = viewModelScope.launch {
+ if (appLockManager.isEnabled().not()) return@launch
+ disableAppLock()
+ .onFailure { error ->
+ broadcastMessages(
+ userId = userId,
+ message = error.getDefaultMessage(appContext, true),
+ type = BroadcastMessage.Type.ERROR,
+ )
+ }
+ .onSuccess {
+ broadcastMessages(
+ userId = userId,
+ message = appContext.getString(BasePresentation.string.app_lock_disabled),
+ type = BroadcastMessage.Type.INFO,
+ )
+ navigateBack()
+ }
+ }
+
+ private fun doEnableSystemLock(navigateBack: () -> Unit) = viewModelScope.launch {
+ if (appLockManager.isEnabled()) return@launch
+ enableAppLock(lockType = AppLockType.SYSTEM)
+ .onFailure { error ->
+ broadcastMessages(
+ userId = userId,
+ message = error.getDefaultMessage(appContext, true),
+ type = BroadcastMessage.Type.ERROR,
+ )
+ }
+ .onSuccess {
+ broadcastMessages(
+ userId = userId,
+ message = appContext.getString(BasePresentation.string.app_lock_enabled),
+ type = BroadcastMessage.Type.INFO,
+ )
+ }
+ navigateBack()
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt
new file mode 100644
index 00000000..3fc4ed6f
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/AutoLockDurationsViewModel.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewmodel
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration
+import me.proton.android.drive.lock.domain.usecase.UpdateAutoLockDuration
+import me.proton.android.drive.ui.viewevent.AutoLockDurationsViewEvent
+import me.proton.android.drive.ui.viewstate.AutoLockDurationsViewState
+import me.proton.core.compose.component.bottomsheet.RunAction
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import javax.inject.Inject
+import kotlin.time.Duration
+import me.proton.core.drive.settings.R as SettingsPresentation
+
+@HiltViewModel
+class AutoLockDurationsViewModel @Inject constructor(
+ @ApplicationContext appContext: Context,
+ configurationProvider: ConfigurationProvider,
+ getAutoLockDuration: GetAutoLockDuration,
+ private val updateAutoLockDuration: UpdateAutoLockDuration,
+) : ViewModel() {
+ val initialViewState: AutoLockDurationsViewState = AutoLockDurationsViewState(
+ title = appContext.getString(SettingsPresentation.string.settings_auto_lock),
+ durations = configurationProvider.autoLockDurations,
+ selected = configurationProvider.autoLockDurations.first(),
+ )
+ val viewState: Flow = getAutoLockDuration().map { duration ->
+ initialViewState.copy(
+ selected = duration
+ )
+ }
+
+ fun viewEvent(
+ runAction: RunAction,
+ dismiss: () -> Unit,
+ ): AutoLockDurationsViewEvent = object : AutoLockDurationsViewEvent {
+ override val onDuration: (Duration) -> Unit = { duration ->
+ runAction {
+ viewModelScope.launch {
+ updateAutoLockDuration(duration)
+ dismiss()
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt
index 9767d5e4..01939388 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/BugReportViewModel.kt
@@ -23,10 +23,14 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
import me.proton.core.accountmanager.domain.AccountManager
import me.proton.core.accountmanager.domain.getPrimaryAccount
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
+import me.proton.core.drive.base.domain.usecase.GetUserEmail
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
import me.proton.core.report.presentation.ReportOrchestrator
import me.proton.core.report.presentation.entity.BugReportInput
@@ -38,6 +42,7 @@ class BugReportViewModel @Inject constructor(
accountManager: AccountManager,
private val reportOrchestrator: ReportOrchestrator,
private val broadcastMessages: BroadcastMessages,
+ private val getUserEmail: GetUserEmail,
) : ViewModel() {
private val primaryAccount = accountManager
@@ -57,11 +62,12 @@ class BugReportViewModel @Inject constructor(
}
fun sendBugReport() {
- primaryAccount.value?.let { account ->
+ viewModelScope.launch {
+ val account = primaryAccount.filterNotNull().first()
reportOrchestrator.startBugReport(
input = BugReportInput(
- email = account.email.orEmpty(),
- username = account.username
+ email = getUserEmail(account.userId),
+ username = account.username,
),
)
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt
index 1d3139a9..b449f6a7 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/FilesViewModel.kt
@@ -36,7 +36,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
@@ -44,6 +43,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.R
@@ -66,9 +66,9 @@ import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLin
import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
import me.proton.core.drive.drivelink.download.domain.usecase.GetDownloadProgress
-import me.proton.core.drive.drivelink.list.domain.usecase.GetDriveLinks
import me.proton.core.drive.drivelink.list.domain.usecase.GetPagedDriveLinksList
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
+import me.proton.core.drive.drivelink.selection.domain.usecase.SelectAll
import me.proton.core.drive.files.presentation.event.FilesViewEvent
import me.proton.core.drive.files.presentation.state.FilesViewState
import me.proton.core.drive.files.presentation.state.ListContentAppendingState
@@ -110,14 +110,15 @@ class FilesViewModel @Inject constructor(
private val getUploadFileLinks: GetUploadFileLinks,
private val getUploadProgress: GetUploadProgress,
private val onFilesDriveLinkError: OnFilesDriveLinkError,
- private val getDriveLinks: GetDriveLinks,
private val selectLinks: SelectLinks,
+ private val selectAll: SelectAll,
private val deselectLinks: DeselectLinks,
private val getSelectedDriveLinks: GetSelectedDriveLinks,
private val savedStateHandle: SavedStateHandle,
getSorting: GetSorting,
private val configurationProvider: ConfigurationProvider,
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle), HomeTabViewModel {
+
private val shareId = savedStateHandle.get(Screen.Files.SHARE_ID)
private val folderId = savedStateHandle.get(Screen.Files.FOLDER_ID)?.let { folderId ->
shareId?.let { FolderId(ShareId(userId, shareId), folderId) }
@@ -141,9 +142,6 @@ class FilesViewModel @Inject constructor(
}
)
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
- private val encryptedDriveLinks: Flow> =
- driveLink.filterNotNull()
- .flatMapLatest { folder -> getDriveLinks(folder.id) }
val driveLinks: Flow> =
driveLink.filterNotNull()
@@ -175,9 +173,9 @@ class FilesViewModel @Inject constructor(
contentDescriptionResId = BasePresentation.string.content_description_select_all,
onAction = {
viewModelScope.launch {
- addSelected(
- encryptedDriveLinks.first().map { driveLink -> driveLink.id }
- )
+ driveLink.value?.let { parent ->
+ selectAll(parent.id, selectionId.value)
+ }
}
}
)
@@ -187,23 +185,20 @@ class FilesViewModel @Inject constructor(
onAction = { viewEvent?.onSelectedOptions?.invoke() }
)
private val topBarActions: MutableStateFlow> = MutableStateFlow(setOf(addFilesAction))
- private val selected: StateFlow> = combine(
- selectionId.filterNotNull().flatMapLatest { selectionId -> getSelectedDriveLinks(selectionId) },
- encryptedDriveLinks,
- ) { selectedDriveLinks, availableDriveLinks ->
- val selectedDriveLinkIds = selectedDriveLinks.map { driveLink -> driveLink.id }.toSet()
- val availableDriveLinkIds = availableDriveLinks.map { driveLink -> driveLink.id }.toSet()
- val removedDriveLinkIds = selectedDriveLinkIds.subtract(availableDriveLinkIds)
- if (removedDriveLinkIds.isNotEmpty()) {
- removeSelected(removedDriveLinkIds.toList())
- }
- if (selectedDriveLinkIds.isEmpty()) {
- topBarActions.value = setOf(addFilesAction)
- } else {
- topBarActions.value = setOf(selectAllAction, selectedOptionsAction)
- }
- selectedDriveLinkIds
- }.stateIn(viewModelScope, SharingStarted.Eagerly, setOf())
+ private val selected: StateFlow> = selectionId
+ .filterNotNull()
+ .transformLatest { id ->
+ emitAll(
+ getSelectedDriveLinks(id).map { driveLinks ->
+ if (driveLinks.isEmpty()) {
+ topBarActions.value = setOf(addFilesAction)
+ } else {
+ topBarActions.value = setOf(selectAllAction, selectedOptionsAction)
+ }
+ driveLinks.map { driveLink -> driveLink.id }.toSet()
+ }
+ )
+ }.stateIn(viewModelScope, SharingStarted.Eagerly, emptySet())
val isBottomNavigationEnabled: Flow = selected.map { set -> set.isEmpty() }
val initialViewState = FilesViewState(
title = savedStateHandle[Screen.Files.FOLDER_NAME],
@@ -269,7 +264,6 @@ class FilesViewModel @Inject constructor(
actionResId = BasePresentation.string.action_empty_files_add_files,
)
private var viewEvent: FilesViewEvent? = null
-
fun viewEvent(
navigateToFiles: (folderId: FolderId, folderName: String?) -> Unit,
navigateToPreview: (fileId: FileId) -> Unit,
@@ -279,6 +273,15 @@ class FilesViewModel @Inject constructor(
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
navigateBack: () -> Unit,
): FilesViewEvent = object : FilesViewEvent {
+
+ private val driveLinkShareFlow = MutableSharedFlow(extraBufferCapacity = 1).also { flow ->
+ viewModelScope.launch {
+ flow.take(1).collect { driveLink ->
+ driveLink.onClick(navigateToFiles, navigateToPreview)
+ }
+ }
+ }
+
override val onTopAppBarNavigation = {
if (selected.value.isNotEmpty()) {
selectionId.value?.let { viewModelScope.launch { deselectLinks(it) } }
@@ -301,7 +304,8 @@ class FilesViewModel @Inject constructor(
addSelected(listOf(driveLink.id))
}
} else {
- driveLink.onClick(navigateToFiles, navigateToPreview)
+ driveLinkShareFlow.tryEmit(driveLink)
+ Unit
}
}
override val onLoadState = onLoadState(
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt
index b7e84837..44632cd9 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/MoveToFolderViewModel.kt
@@ -141,13 +141,13 @@ class MoveToFolderViewModel @Inject constructor(
override val onAppendErrorAction: () -> Unit = this@MoveToFolderViewModel.onRetry
}
- fun confirmMove() {
+ suspend fun confirmMove() {
parentLink.value?.let { folder ->
if (folder.id != parentId) {
viewModelScope.launch {
moveFile(userId, driveLinksToMove.value.map { driveLink -> driveLink.id }, folder.id)
selectionId?.let{ deselectLinks(selectionId) }
- }
+ }.join()
}
}
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt
index e3590fb0..d5ed07ea 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/OfflineViewModel.kt
@@ -25,6 +25,7 @@ import androidx.paging.CombinedLoadStates
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@@ -34,16 +35,23 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
+import me.proton.android.drive.ui.common.onClick
import me.proton.android.drive.ui.navigation.PagerType
import me.proton.android.drive.ui.navigation.Screen
import me.proton.android.drive.ui.viewevent.OfflineViewEvent
import me.proton.android.drive.ui.viewstate.OfflineViewState
import me.proton.core.domain.arch.onSuccess
import me.proton.core.drive.base.domain.entity.Percentage
+import me.proton.core.drive.base.domain.entity.onProcessing
+import me.proton.core.drive.base.domain.extension.onFailure
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
+import me.proton.core.drive.base.presentation.extension.log
+import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
import me.proton.core.drive.drivelink.domain.entity.DriveLink
+import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
import me.proton.core.drive.drivelink.download.domain.usecase.GetDownloadProgress
import me.proton.core.drive.drivelink.list.domain.usecase.GetSortedDecryptedDriveLinks
import me.proton.core.drive.drivelink.offline.domain.usecase.GetDecryptedOfflineDriveLinks
@@ -53,12 +61,6 @@ import me.proton.core.drive.files.presentation.state.ListContentState
import me.proton.core.drive.link.domain.entity.FileId
import me.proton.core.drive.link.domain.entity.FolderId
import me.proton.core.drive.link.domain.entity.LinkId
-import me.proton.android.drive.ui.common.onClick
-import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
-import me.proton.core.drive.base.domain.entity.onProcessing
-import me.proton.core.drive.base.domain.extension.onFailure
-import me.proton.core.drive.base.presentation.extension.log
-import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
import me.proton.core.drive.share.domain.entity.ShareId
import me.proton.core.drive.sorting.domain.entity.Sorting
import me.proton.core.drive.sorting.domain.usecase.GetSorting
@@ -165,20 +167,30 @@ class OfflineViewModel @Inject constructor(
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
navigateBack: () -> Unit,
): OfflineViewEvent = object : OfflineViewEvent {
+
+ private val driveLinkShareFlow = MutableSharedFlow(extraBufferCapacity = 1).also { flow ->
+ viewModelScope.launch {
+ flow.take(1).collect { driveLink ->
+ driveLink.onClick(
+ navigateToFolder = navigateToFiles,
+ navigateToPreview = { fileId ->
+ navigateToPreview(
+ if (folderId == null) PagerType.OFFLINE else PagerType.FOLDER,
+ fileId
+ )
+ }
+ )
+ }
+ }
+ }
+
override val onTopAppBarNavigation = {
navigateBack()
}
override val onSorting = navigateToSortingDialog
override val onDriveLink = { driveLink: DriveLink ->
- driveLink.onClick(
- navigateToFolder = navigateToFiles,
- navigateToPreview = { fileId ->
- navigateToPreview(
- if (folderId == null) PagerType.OFFLINE else PagerType.FOLDER,
- fileId
- )
- }
- )
+ driveLinkShareFlow.tryEmit(driveLink)
+ Unit
}
override val onLoadState = { _: CombinedLoadStates, _: Int -> }
override val onMoreOptions = { driveLink: DriveLink -> navigateToFileOrFolderOptions(driveLink.id) }
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt
index 2de880eb..e9f0d95e 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/PreviewViewModel.kt
@@ -18,11 +18,14 @@
package me.proton.android.drive.ui.viewmodel
+import android.annotation.SuppressLint
+import android.content.Context
import android.net.Uri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -32,6 +35,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -41,6 +45,7 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
+import me.proton.android.drive.extension.getDefaultMessage
import me.proton.android.drive.ui.effect.PreviewEffect
import me.proton.android.drive.ui.navigation.PagerType
import me.proton.android.drive.ui.navigation.Screen
@@ -51,11 +56,12 @@ import me.proton.core.domain.arch.transformSuccess
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.domain.extension.asSuccess
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.base.presentation.entity.toFileTypeCategory
import me.proton.core.drive.base.presentation.extension.require
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
-import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentUri
+import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
import me.proton.core.drive.drivelink.download.domain.usecase.GetFile
@@ -66,7 +72,6 @@ import me.proton.core.drive.files.preview.presentation.component.event.PreviewVi
import me.proton.core.drive.files.preview.presentation.component.state.ContentState
import me.proton.core.drive.files.preview.presentation.component.state.PreviewContentState
import me.proton.core.drive.files.preview.presentation.component.state.PreviewViewState
-import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect
import me.proton.core.drive.files.preview.presentation.component.toComposable
import me.proton.core.drive.link.domain.entity.FileId
import me.proton.core.drive.link.domain.entity.LinkId
@@ -77,8 +82,11 @@ import me.proton.core.drive.base.presentation.R as BasePresentation
import me.proton.core.presentation.R as CorePresentation
@HiltViewModel
+@SuppressLint("StaticFieldLeak")
@OptIn(ExperimentalCoroutinesApi::class)
class PreviewViewModel @Inject constructor(
+ @ApplicationContext private val appContext: Context,
+ private val configurationProvider: ConfigurationProvider,
getDriveLink: GetDecryptedDriveLink,
private val getFile: GetFile,
private val getDocumentUri: GetDocumentUri,
@@ -110,11 +118,12 @@ class PreviewViewModel @Inject constructor(
PagerType.OFFLINE -> OfflineContentProvider(userId, getOfflineDriveNodes)
}
+ private val contentStatesCache = mutableMapOf>()
+
private val driveLinks: StateFlow?> = provider.getDriveLinks()
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private val _previewEffect = MutableSharedFlow()
- private val _zoomEffect = MutableSharedFlow()
private val isFullscreen = MutableStateFlow(false)
private val renderFailed = MutableStateFlow(null)
val initialViewState = PreviewViewState(
@@ -151,7 +160,6 @@ class PreviewViewModel @Inject constructor(
val previewEffect: Flow = _previewEffect.asSharedFlow()
.onStart { emit(PreviewEffect.Fullscreen(isFullscreen.value)) }
- val zoomEffect: Flow = _zoomEffect.asSharedFlow()
fun viewEvent(
navigateBack: () -> Unit,
@@ -160,7 +168,6 @@ class PreviewViewModel @Inject constructor(
override val onTopAppBarNavigation = { navigateBack() }
override val onMoreOptions = { navigateToFileOrFolderOptions(fileId) }
override val onSingleTap = { toggleFullscreen() }
- override val onDoubleTap = { resetZoom() }
override val onRenderFailed = { throwable: Throwable -> renderFailed.value = throwable }
override val mediaControllerVisibility = { visible: Boolean ->
if ((visible && isFullscreen.value) || (!visible && !isFullscreen.value)) {
@@ -197,48 +204,37 @@ class PreviewViewModel @Inject constructor(
}
}
- private fun resetZoom() {
- viewModelScope.launch {
- _zoomEffect.emit(ZoomEffect.Reset)
- }
- }
-
private fun getContentState(
getFileState: GetFile.State,
renderFailed: Throwable? = null,
): ContentState {
return renderFailed?.let { throwable ->
- ContentState.Error.NonRetryable(throwable.message, 0)
+ ContentState.Error.NonRetryable(
+ message = throwable.getDefaultMessage(
+ context = appContext,
+ useExceptionMessage = configurationProvider.useExceptionMessage,
+ ),
+ messageResId = 0,
+ )
} ?: getFileState.toContentState(this)
}
fun getUri(fileId: FileId) = getDocumentUri(userId, fileId)
-
- private fun DriveLink.File.getContentStateFlow(): Flow {
- if (mimeType.toFileTypeCategory().toComposable() == PreviewComposable.Unknown) {
- return NO_PREVIEW_SUPPORTED
- }
- var savedFlow: Flow? = null
- return trigger.flatMapLatest { trigger ->
- val availableFlow = savedFlow
- when {
- availableFlow != null -> availableFlow
- trigger.fileId == id -> {
+ private fun DriveLink.File.getContentStateFlow(): Flow =
+ contentStatesCache.getOrPut(id) {
+ if (mimeType.toFileTypeCategory().toComposable() == PreviewComposable.Unknown) {
+ NO_PREVIEW_SUPPORTED
+ } else {
+ trigger.filter { trigger -> trigger.fileId == id }.flatMapLatest { trigger ->
combine(
getFile(this, trigger.verifySignature),
renderFailed,
) { fileState, renderFailed ->
- getContentState(fileState, renderFailed).also { state ->
- if (state is ContentState.Available) {
- savedFlow = flowOf(state)
- }
- }
+ getContentState(fileState, renderFailed)
}
}
- else -> DEFAULT_STATE
}
}
- }
private data class Trigger(
val fileId: FileId,
@@ -247,7 +243,6 @@ class PreviewViewModel @Inject constructor(
companion object {
private val NO_PREVIEW_SUPPORTED = flowOf(ContentState.Available(Uri.EMPTY))
- private val DEFAULT_STATE = flowOf(ContentState.Downloading(null))
}
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt
index 86a7f35d..8535b3a2 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SettingsViewModel.kt
@@ -23,6 +23,7 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
+import android.os.Build
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@@ -32,12 +33,13 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import me.proton.android.drive.BuildConfig
import me.proton.android.drive.R
-import me.proton.core.presentation.R as CorePresentation
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.android.drive.lock.domain.usecase.GetAutoLockDuration
+import me.proton.android.drive.lock.domain.usecase.HasEnableAppLockTimestamp
import me.proton.android.drive.settings.DebugSettings
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
import me.proton.core.drive.settings.presentation.component.DebugSettingsStateAndEvent
@@ -46,10 +48,13 @@ import me.proton.core.drive.settings.presentation.event.SettingsViewEvent
import me.proton.core.drive.settings.presentation.state.DebugSettingsViewState
import me.proton.core.drive.settings.presentation.state.LegalLink
import me.proton.core.drive.settings.presentation.state.SettingsViewState
+import me.proton.drive.android.settings.domain.entity.ThemeStyle
import me.proton.drive.android.settings.domain.usecase.GetThemeStyle
import me.proton.drive.android.settings.domain.usecase.UpdateThemeStyle
-import me.proton.drive.android.settings.domain.entity.ThemeStyle
import javax.inject.Inject
+import me.proton.core.drive.base.domain.extension.combine as baseCombine
+import me.proton.core.drive.base.presentation.R as BasePresentation
+import me.proton.core.presentation.R as CorePresentation
@HiltViewModel
@SuppressLint("StaticFieldLeak")
@@ -59,18 +64,23 @@ class SettingsViewModel @Inject constructor(
getThemeStyle: GetThemeStyle,
private val updateThemeStyle: UpdateThemeStyle,
savedStateHandle: SavedStateHandle,
+ appLockManager: AppLockManager,
+ getAutoLockDuration: GetAutoLockDuration,
+ private val hasEnableAppLockTimestamp: HasEnableAppLockTimestamp,
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
private val _errorMessage = MutableSharedFlow()
val errorMessage: SharedFlow = _errorMessage
- val viewState: Flow = combine(
+ val viewState: Flow = baseCombine(
debugSettings.baseUrlFlow,
debugSettings.hostFlow,
debugSettings.appVersionHeaderFlow,
debugSettings.useExceptionMessageFlow,
getThemeStyle(userId),
- ) { baseUrl, host, appVersionHeader, useExceptionMessage, themeStyle ->
+ appLockManager.enabled,
+ getAutoLockDuration(),
+ ) { baseUrl, host, appVersionHeader, useExceptionMessage, themeStyle, enabled, autoLockDuration ->
SettingsViewState(
navigationIcon = CorePresentation.drawable.ic_arrow_back,
appNameResId = R.string.app_name,
@@ -92,11 +102,18 @@ class SettingsViewModel @Inject constructor(
baseUrl = baseUrl,
appVersionHeader = appVersionHeader,
useExceptionMessage = useExceptionMessage,
- )
+ ),
+ appAccessSubtitleResId = getAppAccessSubtitleResId(enabled),
+ isAutoLockDurationsVisible = enabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
+ autoLockDuration = autoLockDuration,
)
}.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
- fun viewEvent(navigateBack: () -> Unit) = SettingsViewEvent(
+ fun viewEvent(
+ navigateBack: () -> Unit,
+ navigateToAppAccess: () -> Unit,
+ navigateToAutoLockDurations: () -> Unit,
+ ) = SettingsViewEvent(
navigateBack = navigateBack,
onLinkClicked = { link ->
when (link) {
@@ -107,9 +124,21 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch {
updateThemeStyle(userId, enumValues().first { style -> style.resId == newStyle })
}
+ },
+ onAppAccess = {
+ navigateToAppAccess()
+ },
+ onAutoLockDurations = {
+ navigateToAutoLockDurations()
}
)
+ private suspend fun getAppAccessSubtitleResId(isAppLockEnabled: Boolean): Int = when {
+ isAppLockEnabled -> BasePresentation.string.app_lock_option_system
+ hasEnableAppLockTimestamp().not() -> BasePresentation.string.app_lock_option_never_set
+ else -> BasePresentation.string.app_lock_option_none
+ }
+
private fun onExternalLinkClicked(link: LegalLink.External) {
try {
context.startActivity(
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt
index 72b59a0a..a6e6fbbf 100644
--- a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SharedViewModel.kt
@@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
import me.proton.android.drive.ui.common.onClick
@@ -114,7 +115,7 @@ class SharedViewModel @Inject constructor(
}.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = DataResult.Processing(ResponseSource.Local))
val driveLinksFlow = driveLinks.mapSuccessValueOrNull()
- .map { driveLinks -> driveLinks.orEmpty() }
+ .filterNotNull()
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val initialViewState = SharedViewState(
@@ -146,7 +147,11 @@ class SharedViewModel @Inject constructor(
}.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
private fun DataResult>.toListContentState(): ListContentState = when (val cause = this) {
- is DataResult.Processing -> ListContentState.Loading
+ is DataResult.Processing -> if (driveLinksFlow.replayCache.isNotEmpty()) {
+ ListContentState.Content(true)
+ } else {
+ ListContentState.Loading
+ }
is DataResult.Success -> if (cause.value.isNotEmpty()) {
ListContentState.Content(false)
} else {
@@ -171,18 +176,22 @@ class SharedViewModel @Inject constructor(
navigateToSortingDialog: (Sorting) -> Unit,
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
): SharedViewEvent = object : SharedViewEvent {
+
+ private val driveLinkShareFlow = MutableSharedFlow(extraBufferCapacity = 1).also { flow ->
+ viewModelScope.launch {
+ flow.take(1).collect { driveLink ->
+ driveLink.onClick(navigateToFiles, navigateToPreview)
+ }
+ }
+ }
override val onTopAppBarNavigation = {
viewModelScope.launch { _effects.emit(HomeEffect.OpenDrawer) }
Unit
}
override val onSorting = navigateToSortingDialog
- override val onDriveLink = { driveNode: DriveLink ->
- driveNode.onClick(
- navigateToFolder = navigateToFiles,
- navigateToPreview = { fileId ->
- navigateToPreview(fileId)
- }
- )
+ override val onDriveLink = { driveLink: DriveLink ->
+ driveLinkShareFlow.tryEmit(driveLink)
+ Unit
}
override val onLoadState = { _: CombinedLoadStates, _: Int -> }
override val onMoreOptions = { driveLink: DriveLink -> navigateToFileOrFolderOptions(driveLink.id) }
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt
new file mode 100644
index 00000000..58085b9c
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewmodel/SystemAccessDialogViewModel.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewmodel
+
+import android.annotation.TargetApi
+import android.app.admin.DevicePolicyManager
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.provider.Settings
+import androidx.lifecycle.ViewModel
+import me.proton.android.drive.ui.viewevent.SystemAccessDialogViewEvent
+import javax.inject.Inject
+
+class SystemAccessDialogViewModel @Inject constructor(
+
+) : ViewModel() {
+ fun viewEvent(
+ context: Context,
+ dismiss: () -> Unit,
+ ) = object : SystemAccessDialogViewEvent {
+ override val onSettings = {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ context.showBiometricSettings()
+ } else {
+ context.showSetNewPasswordSettings()
+ }
+ dismiss()
+ }
+ }
+
+ private fun Context.showSetNewPasswordSettings() {
+ startActivity(Intent(DevicePolicyManager.ACTION_SET_NEW_PASSWORD))
+ }
+
+ @TargetApi(Build.VERSION_CODES.P)
+ private fun Context.showBiometricSettings() {
+ val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ Settings.ACTION_BIOMETRIC_ENROLL
+ } else {
+ @Suppress("DEPRECATION")
+ Settings.ACTION_FINGERPRINT_ENROLL
+ }
+ startActivity(Intent(action))
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt
new file mode 100644
index 00000000..9b78a7aa
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AppAccessViewState.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewstate
+
+import androidx.compose.runtime.Immutable
+
+@Immutable
+data class AppAccessViewState(
+ val title: String,
+ val enabledOption: AccessOption,
+)
+
+enum class AccessOption {
+ NONE,
+ SYSTEM,
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt
new file mode 100644
index 00000000..27ea18b9
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/ui/viewstate/AutoLockDurationsViewState.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.ui.viewstate
+
+import kotlin.time.Duration
+
+data class AutoLockDurationsViewState(
+ val title: String,
+ val durations: Set,
+ val selected: Duration,
+)
diff --git a/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt b/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt
index d7ed4580..f457a5bc 100644
--- a/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt
+++ b/app/src/main/kotlin/me/proton/android/drive/usecase/CleanUpAccount.kt
@@ -18,6 +18,7 @@
package me.proton.android.drive.usecase
+import me.proton.android.drive.lock.domain.usecase.DisableAppLock
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.domain.usecase.DeleteAllFolders
import me.proton.core.drive.drivelink.crypto.domain.usecase.RemoveAllDecryptedText
@@ -28,11 +29,13 @@ class CleanUpAccount @Inject constructor(
private val deleteAllFolders: DeleteAllFolders,
private val removeAllKeys: RemoveAllKeys,
private val removeAllDecryptedText: RemoveAllDecryptedText,
+ private val disableAppLock: DisableAppLock,
) {
suspend operator fun invoke(userId: UserId) {
deleteAllFolders(userId)
removeAllKeys(userId)
removeAllDecryptedText(userId)
+ disableAppLock(userAuthenticationRequired = false)
}
}
diff --git a/app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt b/app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt
new file mode 100644
index 00000000..d7606952
--- /dev/null
+++ b/app/src/main/kotlin/me/proton/android/drive/usecase/GetDocumentsProviderRootsImpl.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Drive.
+ *
+ * Proton Drive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Drive 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 Proton Drive. If not, see .
+ */
+
+package me.proton.android.drive.usecase
+
+import kotlinx.coroutines.flow.first
+import me.proton.android.drive.lock.domain.manager.AppLockManager
+import me.proton.core.account.domain.entity.Account
+import me.proton.core.accountmanager.domain.AccountManager
+import me.proton.core.drive.documentsprovider.domain.usecase.GetDocumentsProviderRoots
+import javax.inject.Inject
+
+class GetDocumentsProviderRootsImpl @Inject constructor(
+ private val appLockManager: AppLockManager,
+ private val accountManager: AccountManager,
+) : GetDocumentsProviderRoots {
+
+ override suspend fun invoke(): List = takeUnless { appLockManager.isEnabled() }
+ ?.let { accountManager.getAccounts().first() }.orEmpty()
+}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt
index 669870b8..f8d4da7d 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/CreateFolderRobot.kt
@@ -18,34 +18,22 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.hasAnyAncestor
-import androidx.compose.ui.test.hasSetTextAction
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
-import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag
import me.proton.core.drive.folder.create.presentation.CreateFolderComponentTestTag
+import me.proton.test.fusion.Fusion.node
import me.proton.core.drive.base.presentation.R as BasePresentation
object CreateFolderRobot : Robot {
- private val createFolderScreen get() = node(hasTestTag(CreateFolderComponentTestTag.screen))
- private val cancelButton get() = node(hasTextResource(BasePresentation.string.common_cancel_action))
- private val createButton get() = node(hasTextResource(BasePresentation.string.common_create_action))
- private val folderNameField
- get() = node(
- hasSetTextAction(),
- hasAnyAncestor(hasTestTag(CreateFolderComponentTestTag.folderNameTextField))
- )
+ private val createFolderScreen get() = node.withTag(CreateFolderComponentTestTag.screen)
+ private val cancelButton get() = node.withText(BasePresentation.string.common_cancel_action)
+ private val createButton get() = node.withText(BasePresentation.string.common_create_action)
+ private val folderNameField get() = node.isSetText().hasAncestor(
+ node.withTag(CreateFolderComponentTestTag.folderNameTextField)
+ )
- fun clickCancel() = cancelButton.tryToClickAndGoTo(FilesTabRobot)
- fun typeFolderName(text: String) = folderNameField.tryToTypeText(text, CreateFolderRobot)
- fun clearName() = folderNameField.clearText(CreateFolderRobot)
-
- fun clickCreate() {
- createButton.tryToClickAndGoTo(this)
- }
+ fun clickCancel() = cancelButton.clickTo(FilesTabRobot)
+ fun typeFolderName(text: String) = apply { folderNameField.typeText(text) }
+ fun clearName() = apply { folderNameField.clearText() }
+ fun clickCreate() = Unit.also { createButton.clickTo(this) }
override fun robotDisplayed() {
createFolderScreen.assertIsDisplayed()
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt
index e844d495..4e45a904 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FileFolderOptionsRobot.kt
@@ -18,22 +18,22 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.hasTestTag
+import me.proton.test.fusion.Fusion.node
import me.proton.android.drive.ui.dialog.FileFolderOptionsDialogTestTag
import me.proton.core.drive.base.presentation.R
-object FileFolderOptionsRobot : Robot {
- private val fileFolderOptionsScreen get() = node(hasTestTag(FileFolderOptionsDialogTestTag.fileOrFolderOptions))
- private val moveButton get() = node(hasTextResource(R.string.files_move_file_action))
- private val moveToTrash get() = node(hasTextResource(R.string.files_send_to_trash_action))
- private val makeAvailableOfflineButton get() = node(hasTextResource(R.string.title_make_available_offline))
- private val getLinkButton get() = node(hasTextResource(R.string.title_get_link))
- private val renameButton get() = node(hasTextResource(R.string.files_rename_file_action))
- private val folderDetailsButton get() = node(hasTextResource(R.string.files_display_folder_info_action))
- fun clickMove() = moveButton.tryToClickAndGoTo(MoveToFolderRobot)
- fun clickRename() = renameButton.tryToClickAndGoTo(RenameRobot)
+object FileFolderOptionsRobot : Robot {
+ private val fileFolderOptionsScreen get() = node.withTag(FileFolderOptionsDialogTestTag.fileOrFolderOptions)
+ private val moveButton get() = node.withText(R.string.files_move_file_action)
+ private val moveToTrash get() = node.withText(R.string.files_send_to_trash_action)
+ private val makeAvailableOfflineButton get() = node.withText(R.string.title_make_available_offline)
+ private val getLinkButton get() = node.withText(R.string.title_get_link)
+ private val renameButton get() = node.withText(R.string.files_rename_file_action)
+ private val folderDetailsButton get() = node.withText(R.string.files_display_folder_info_action)
+
+ fun clickMove() = moveButton.clickTo(MoveToFolderRobot)
+ fun clickRename() = renameButton.clickTo(RenameRobot)
override fun robotDisplayed() {
fileFolderOptionsScreen.assertIsDisplayed()
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt
index d82d62a9..3db5ce04 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/FilesTabRobot.kt
@@ -18,76 +18,59 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertIsSelected
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.hasAnyChild
-import androidx.compose.ui.test.hasAnySibling
-import androidx.compose.ui.test.hasContentDescription
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.hasTextExactly
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeUp
+
import me.proton.core.drive.base.presentation.R
import me.proton.core.drive.files.presentation.component.FilesTestTag
-import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag
+import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag.threeDotsButton
+import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag.item
import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag.ItemType
-import me.proton.core.test.android.instrumented.utils.StringUtils
+import me.proton.test.fusion.Fusion.allNodes
+import me.proton.test.fusion.Fusion.node
+import me.proton.test.fusion.ui.common.enums.SwipeDirection
object FilesTabRobot : HomeRobot {
- private val uploadFileDesc =
- StringUtils.stringFromResource(R.string.files_upload_content_description_upload_file)
- private val plusButton get() = node(hasContentDescription(uploadFileDesc))
- private val filesListItems get() = nodes(hasTestTag(FilesListItemComponentTestTag.item))
-
+ private val plusButton get() = node.withContentDescription(R.string.files_upload_content_description_upload_file)
+ private fun itemWithName(name: String) = node.withTag(item).withText(name)
+ private val fileList get() = node.withTag(FilesTestTag.content)
private fun moreButton(itemName: String, itemType: ItemType) =
- node(
- hasTestTag(FilesListItemComponentTestTag.threeDotsButton(itemType)),
- hasAnySibling(hasTextExactly(itemName))
- )
-
- private val filesContent get() = node(hasTestTag(FilesTestTag.content))
+ node
+ .withTag(threeDotsButton(itemType))
+ .hasSibling(node.withText(itemName))
fun itemWithTextDisplayed(text: String) {
- filesListItems.filter(hasText(text)).assertCountEquals(1)
+ itemWithName(text).await { assertIsDisplayed() }
}
- fun swipeUpToItemWithName(itemName: String): FilesTabRobot = waitFor(this) {
- try {
- node(hasText(itemName)).assertIsDisplayed()
- } catch (error: AssertionError) {
- filesContent.performTouchInput { swipeUp(durationMillis = 1000L) }
- throw error
- }
+ fun scrollToItemWithName(itemName: String): FilesTabRobot = apply {
+ allNodes.withTag(item).assertAny(node.isEnabled())
+ fileList.scrollTo(node.withText(itemName))
}
- fun clickPlusButton() = plusButton.tryToClickAndGoTo(ParentFolderOptionsRobot)
+ fun clickPlusButton() = plusButton.clickTo(ParentFolderOptionsRobot)
fun clickMoreOnItem(title: String) =
- node(
- hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.File)) or hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.Folder)),
- hasAnySibling(hasTextExactly(title))
- ).tryToClickAndGoTo(FileFolderOptionsRobot)
+ node
+ .withAnyTag(threeDotsButton(ItemType.File), threeDotsButton(ItemType.Folder))
+ .hasSibling(node.withText(title))
+ .clickTo(FileFolderOptionsRobot)
fun clickMoreOnFolder(title: String) =
- moreButton(title, ItemType.Folder).tryToClickAndGoTo(FileFolderOptionsRobot)
+ moreButton(title, ItemType.Folder).clickTo(FileFolderOptionsRobot)
fun clickMoreOnFile(title: String) =
- moreButton(title, ItemType.File).tryToClickAndGoTo(FileFolderOptionsRobot)
+ moreButton(title, ItemType.File).clickTo(FileFolderOptionsRobot)
fun clickOnFile(name: String) =
- node(
- hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.File)),
- hasAnyChild(hasText(name))
- ).tryToClickAndGoTo(PreviewRobot)
+ node
+ .withTag(threeDotsButton(ItemType.File))
+ .hasChild(node.withText(name))
+ .clickTo(this)
fun clickOnFolder(name: String) =
- node(
- hasAnySibling(hasTestTag(FilesListItemComponentTestTag.threeDotsButton(ItemType.Folder))),
- hasText(name)
- ).tryToClickAndGoTo(FilesTabRobot)
+ node
+ .withText(name)
+ .hasSibling(node.withTag(threeDotsButton(ItemType.Folder)))
+ .clickTo(this)
override fun robotDisplayed() {
homeScreenDisplayed()
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt
index d047fd85..3b17e553 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/HomeRobot.kt
@@ -18,28 +18,26 @@
package me.proton.android.drive.ui.robot
+import me.proton.test.fusion.Fusion.node
import androidx.annotation.StringRes
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.hasAnyChild
-import androidx.compose.ui.test.hasTestTag
import me.proton.android.drive.ui.screen.HomeScreenTestTag
import me.proton.core.drive.base.presentation.R
import me.proton.core.drive.base.presentation.component.BottomNavigationComponentTestTag
interface HomeRobot : Robot {
- private val homeScreen get() = node(hasTestTag(HomeScreenTestTag.screen))
+ private val homeScreen get() = node.withTag(HomeScreenTestTag.screen)
val filesTab get() = tabWithText(R.string.title_files)
val sharedTab get() = tabWithText(R.string.title_shared)
- fun clickFilesTab() = filesTab.tryToClickAndGoTo(FilesTabRobot)
- fun clickSharedTab() = sharedTab.tryToClickAndGoTo(SharedTabRobot)
+ fun clickFilesTab() = filesTab.clickTo(FilesTabRobot)
+ fun clickSharedTab() = sharedTab.clickTo(SharedTabRobot)
fun homeScreenDisplayed() {
homeScreen.assertIsDisplayed()
}
- private fun tabWithText(@StringRes textRes: Int) = node(
- hasTestTag(BottomNavigationComponentTestTag.tab),
- hasAnyChild(hasTextResource(textRes)),
- )
+ private fun tabWithText(@StringRes textRes: Int) =
+ node
+ .withTag(BottomNavigationComponentTestTag.tab)
+ .hasChild(node.withText(textRes))
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt
index 12ccdde2..40994157 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/MoveToFolderRobot.kt
@@ -18,38 +18,33 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertCountEquals
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.performTouchInput
-import androidx.compose.ui.test.swipeDown
+import me.proton.test.fusion.Fusion.node
import me.proton.android.drive.R
import me.proton.android.drive.ui.screen.MoveToFolderScreenTestTag
import me.proton.core.drive.files.presentation.component.FilesTestTag
import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag
-import me.proton.core.test.android.instrumented.utils.StringUtils
object MoveToFolderRobot : Robot {
- private val moveToFolderScreen get() = node(hasTestTag(MoveToFolderScreenTestTag.screen))
- private val addFolderButton get() = node(hasTestTag(MoveToFolderScreenTestTag.plusFolderButton))
- private val cancelButton get() = node(hasText(StringUtils.stringFromResource(R.string.move_file_dismiss_action)))
- private val moveButton get() = node(hasText(StringUtils.stringFromResource(R.string.move_file_confirm_action)))
- private val filesListItems get() = nodes(hasTestTag(FilesListItemComponentTestTag.item))
- private val filesContent get() = node(hasTestTag(FilesTestTag.content))
+ private val moveToFolderScreen get() = node.withTag(MoveToFolderScreenTestTag.screen)
+ private val addFolderButton get() = node.withTag(MoveToFolderScreenTestTag.plusFolderButton)
+ private val cancelButton get() = node.withText(R.string.move_file_dismiss_action)
+ private val moveButton get() = node.withText(R.string.move_file_confirm_action)
+ private val fileList get() = node.withTag(FilesTestTag.content)
+ private fun itemWithName(name: String) =
+ node.withTag(FilesListItemComponentTestTag.item).withText(name)
- fun clickAddFolder() = addFolderButton.tryToClickAndGoTo(CreateFolderRobot)
- fun clickCancel() = cancelButton.tryToClickAndGoTo(CreateFolderRobot)
- fun clickMove() = moveButton.tryToClickAndGoTo(CreateFolderRobot)
+ fun clickAddFolder() = addFolderButton.clickTo(CreateFolderRobot)
+ fun clickCancel() = cancelButton.clickTo(CreateFolderRobot)
+ fun clickMove() = moveButton.clickTo(CreateFolderRobot)
- fun swipeDown() = filesContent.tryPerformTouchInputAndGoTo(this) { swipeDown()}
-
- fun itemListWithTextDisplayed(text: String, count: Int = 1) {
- filesListItems.filter(hasText(text)).assertCountEquals(count)
+ fun itemWithTextDisplayed(text: String) {
+ fileList.scrollTo(node.withText(text))
}
override fun robotDisplayed() {
moveToFolderScreen.assertIsDisplayed()
+ addFolderButton.assertIsDisplayed()
+ cancelButton.assertIsDisplayed()
+ moveButton.assertIsDisplayed()
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt
index 7fe79107..44ed4874 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/ParentFolderOptionsRobot.kt
@@ -18,18 +18,19 @@
package me.proton.android.drive.ui.robot
+import me.proton.test.fusion.Fusion.node
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasTestTag
import me.proton.android.drive.R
import me.proton.android.drive.ui.dialog.ParentFolderOptionsDialogTestTag
object ParentFolderOptionsRobot : Robot {
- private val contextMenu get() = node(hasTestTag(ParentFolderOptionsDialogTestTag.contextMenu))
- private val createFolderButton get() = node(hasTextResource(R.string.folder_option_create_folder))
- private val uploadAFileButton get() = node(hasTextResource(R.string.folder_option_import_file))
- private val takePhotoButton get() = node(hasTextResource(R.string.folder_option_take_a_photo))
+ private val contextMenu get() = node.withTag(ParentFolderOptionsDialogTestTag.contextMenu)
+ private val createFolderButton get() = node.withText(R.string.folder_option_create_folder)
+ private val uploadAFileButton get() = node.withText(R.string.folder_option_import_file)
+ private val takePhotoButton get() = node.withText(R.string.folder_option_take_a_photo)
- fun clickCreateFolder() = createFolderButton.tryToClickAndGoTo(CreateFolderRobot)
+ fun clickCreateFolder() = createFolderButton.clickTo(CreateFolderRobot)
override fun robotDisplayed() {
contextMenu.assertIsDisplayed()
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt
index 1d44b90d..76d32c9e 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/PreviewRobot.kt
@@ -18,6 +18,7 @@
package me.proton.android.drive.ui.robot
+import me.proton.test.fusion.Fusion.node
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasTestTag
@@ -25,18 +26,16 @@ import androidx.compose.ui.test.hasText
import me.proton.core.drive.base.presentation.R
import me.proton.core.drive.base.presentation.component.TopAppBarComponentTestTag
import me.proton.core.drive.files.preview.presentation.component.PreviewComponentTestTag
-import me.proton.core.test.android.instrumented.utils.StringUtils
object PreviewRobot : Robot {
- private val contextualButtonDesc = StringUtils.stringFromResource(R.string.content_description_more_options)
- private val previewScreen get() = node(hasTestTag(PreviewComponentTestTag.screen))
- private val contextualButton get() = node(hasContentDescription(contextualButtonDesc))
+ private val previewScreen get() = node.withTag(PreviewComponentTestTag.screen)
+ private val contextualButton get() = node.withContentDescription(R.string.content_description_more_options)
- fun clickOnContextualButton() = contextualButton.tryToClickAndGoTo(FileFolderOptionsRobot)
+ fun clickOnContextualButton() = contextualButton.clickTo(FileFolderOptionsRobot)
fun topBarWithTextDisplayed(itemName: String) {
- node(hasTestTag(TopAppBarComponentTestTag.appBar) and hasText(text = itemName, substring = true)).assertExists()
+ node.withTag(TopAppBarComponentTestTag.appBar).withTextSubstring(itemName).assertExists()
}
override fun robotDisplayed() {
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt
index c7a3e2c1..1517cb5b 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/RenameRobot.kt
@@ -18,28 +18,20 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.hasAnyAncestor
-import androidx.compose.ui.test.hasSetTextAction
-import androidx.compose.ui.test.hasTestTag
+import me.proton.test.fusion.Fusion.node
import me.proton.core.drive.drivelink.rename.presentation.R
import me.proton.core.drive.drivelink.rename.presentation.RenameScreenTestTag
object RenameRobot : Robot {
+ private val renameScreen get() = node.withTag(RenameScreenTestTag.screen)
+ private val cancelRenameButton get() = node.withText(R.string.link_rename_dismiss_button)
+ private val confirmRenameButton get() = node.withText(R.string.link_rename_button)
+ private val renameTextField get() = node.isSetText().hasAncestor(node.withTag(RenameScreenTestTag.textField))
- private val renameScreen get() = node(hasTestTag(RenameScreenTestTag.screen))
- private val cancelRenameButton get() = node(hasTextResource(R.string.link_rename_dismiss_button))
- private val confirmRenameButton get() = node(hasTextResource(R.string.link_rename_button))
- private val renameTextField
- get() = node(
- hasSetTextAction(),
- hasAnyAncestor(hasTestTag(RenameScreenTestTag.textField))
- )
-
- fun clickCancel() = cancelRenameButton.tryToClickAndGoTo(FilesTabRobot)
- fun clickRename() = confirmRenameButton.tryToClickAndGoTo(FilesTabRobot)
- fun typeName(text: String) = renameTextField.tryToTypeText(text, RenameRobot)
- fun clearName() = renameTextField.clearText(RenameRobot)
+ fun clickCancel() = cancelRenameButton.clickTo(FilesTabRobot)
+ fun clickRename() = confirmRenameButton.clickTo(FilesTabRobot)
+ fun typeName(text: String) = apply { renameTextField.typeText(text) }
+ fun clearName() = apply { renameTextField.clearText() }
override fun robotDisplayed() {
renameScreen.assertIsDisplayed()
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt
index 23c9bc0d..8d9034a4 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/Robot.kt
@@ -18,183 +18,23 @@
package me.proton.android.drive.ui.robot
-import android.app.Instrumentation
-import android.graphics.Bitmap
-import androidx.annotation.StringRes
-import androidx.compose.ui.graphics.asAndroidBitmap
-import androidx.compose.ui.test.ComposeTimeoutException
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.SemanticsNodeInteractionCollection
-import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextClearance
-import androidx.compose.ui.test.performTextInput
-import androidx.compose.ui.test.performTouchInput
-import androidx.test.platform.app.InstrumentationRegistry
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
-import me.proton.android.drive.ui.test.BaseTest.Companion.composeTestRule
-import me.proton.android.drive.ui.test.BaseTest.Companion.screenshotLocation
-import me.proton.android.drive.ui.test.BaseTest.Companion.testName
+import me.proton.test.fusion.Fusion.node
import me.proton.core.drive.folder.create.presentation.R
-import me.proton.core.test.android.instrumented.ProtonTest.Companion.testTag
import me.proton.core.test.android.instrumented.utils.StringUtils
-import me.proton.core.util.kotlin.CoreLogger
-import me.proton.core.util.kotlin.EMPTY_STRING
-import java.io.FileNotFoundException
-import java.io.FileOutputStream
-import java.util.concurrent.TimeoutException
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
+import me.proton.test.fusion.ui.compose.builders.OnNode
interface Robot {
- val instrumentation: Instrumentation get() = InstrumentationRegistry.getInstrumentation()
- val waitForScreenDuration: Duration get() = 10.seconds
- val waitForItemToAppearInList: Duration get() = 30.seconds
- val watchInterval: Duration get() = 100.milliseconds
- val shouldUseUnmergedTree: Boolean get() = true
-
fun robotDisplayed()
- fun node(
- vararg semanticsMatchers: SemanticsMatcher,
- useUnmergedTree: Boolean = shouldUseUnmergedTree,
- ): SemanticsNodeInteraction =
- composeTestRule.onNode(getFinalSemanticMatcher(semanticsMatchers), useUnmergedTree)
-
- fun nodes(
- vararg semanticsMatchers: SemanticsMatcher,
- useUnmergedTree: Boolean = shouldUseUnmergedTree,
- ): SemanticsNodeInteractionCollection =
- composeTestRule.onAllNodes(getFinalSemanticMatcher(semanticsMatchers), useUnmergedTree)
-
- private fun getFinalSemanticMatcher(
- semanticsMatchers: Array
- ): SemanticsMatcher {
- var finalSemanticsMatcher = semanticsMatchers.first()
- semanticsMatchers.drop(1).forEach {
- finalSemanticsMatcher = finalSemanticsMatcher.and(it)
- }
- return finalSemanticsMatcher
- }
-
- /**
- * Common semantics matchers
- */
- fun hasTextResource(
- @StringRes resourceId: Int,
- substring: Boolean = false,
- ignoreCase: Boolean = false,
- formatString: String = EMPTY_STRING
- ): SemanticsMatcher = hasText(
- StringUtils.stringFromResource(resourceId, substring, ignoreCase).format(formatString)
- )
-
- private fun successGrowler(itemName: String) = node(
- hasText(
- StringUtils.stringFromResource
- (R.string.folder_create_successful, itemName))
- )
-
- /**
- * Common user actions
- */
- fun SemanticsNodeInteraction.tryToClickAndGoTo(goesTo: T): T =
- waitFor(goesTo) { performClick() }
-
- fun SemanticsNodeInteraction.tryToTypeText(text: String, goesTo: T): T =
- waitFor(goesTo) { performTextInput(text) }
-
- fun SemanticsNodeInteraction.clearText(goesTo: T): T =
- waitFor(goesTo) { performTextClearance() }
-
- fun SemanticsNodeInteraction.tryPerformTouchInputAndGoTo(
- goesTo: T,
- touchInput: TouchInjectionScope.() -> Unit
- ): T = waitFor(goesTo) { performTouchInput { touchInput() } }
+ fun OnNode.clickTo(goesTo: T): T = goesTo.apply { click() }
+ /** Common actions **/
fun dismissSuccessGrowler(itemName: String, goesTo: T) =
- successGrowler(itemName).tryToClickAndGoTo(goesTo)
+ node
+ .withText(StringUtils.stringFromResource(R.string.folder_create_successful, itemName))
+ .clickTo(goesTo)
-
- /**
- * Other extensions and Helpers
- */
- fun waitFor(
- goesTo: T,
- timeout: Duration = waitForScreenDuration,
- interval: Duration = watchInterval,
- block: () -> Any,
- ): T {
- var error: Throwable = TimeoutException("Condition not met in ${timeout}ms")
- try {
- composeTestRule.waitForIdle()
- composeTestRule.waitUntil(timeout.inWholeMilliseconds) {
- try {
- block()
- true
- } catch (e: AssertionError) {
- // Thrown on Compose failed actions and assertions
- error = handleTestError(e, 1)
- runBlocking { delay(interval) }
- false
- } catch (e: IllegalStateException) {
- // Thrown when Compose view is not ready
- error = handleTestError(e, 0)
- runBlocking { delay(interval) }
- false
- }
- }
- } catch (e: ComposeTimeoutException) {
- CoreLogger.e(testTag, e)
- composeTestRule
- .onAllNodes(SemanticsMatcher("isRoot") { it.isRoot })
- .onFirst()
- .screenshot("${screenshotLocation}/${testName.methodName}.png")
- throw error
- }
- return goesTo
- }
-
- private fun handleTestError(throwable: Throwable, messageLine: Int): Throwable {
- val lines = throwable.message?.lines()
- if (!lines.isNullOrEmpty() && messageLine < lines.size) {
- CoreLogger.i(testTag, "Condition not yet met: ${lines[messageLine]}")
- return throwable
- }
- val message = "Could not extract message line at position $messageLine. Printing full message"
- CoreLogger.e(testTag, throwable, message)
- return throwable
- }
-
- private fun SemanticsNodeInteraction.screenshot(file: String) {
- try {
- val bitmap = captureToImage().asAndroidBitmap()
- FileOutputStream(file).use { destination ->
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, destination)
- }
- } catch (throwable: Throwable) {
- when (throwable) {
- is FileNotFoundException -> "File not found"
- is OutOfMemoryError -> "Out of memory"
- else -> "Unknown error"
- }.let {
- CoreLogger.e(testTag, throwable, "Could not take screenshot: $it")
- }
- }
- }
-
- fun nodeWithTextDisplayed(@StringRes stringRes: String) {
- node(hasText(stringRes)).assertIsDisplayed()
- }
-
- fun nodeWithTextResourceDisplayed(@StringRes stringRes: Int, formatAttr: Any = EMPTY_STRING) {
- node(hasTextResource(stringRes, formatString = formatAttr.toString())).assertIsDisplayed()
- }
+ /** Common assertions **/
+ fun nodeWithTextDisplayed(text: String) =
+ node.withText(text).await { assertIsDisplayed() }
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt
index a7089737..52af5630 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/SharedTabRobot.kt
@@ -18,15 +18,13 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertCountEquals
+import me.proton.test.fusion.Fusion.allNodes
+import me.proton.test.fusion.Fusion.node
import androidx.compose.ui.test.assertIsSelected
-import androidx.compose.ui.test.filter
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.hasText
import me.proton.core.drive.files.presentation.component.files.FilesListItemComponentTestTag
object SharedTabRobot : HomeRobot {
- private val filesListItems get() = nodes(hasTestTag(FilesListItemComponentTestTag.item))
+ private val filesListItems get() = allNodes.withTag(FilesListItemComponentTestTag.item)
override fun robotDisplayed() {
homeScreenDisplayed()
@@ -34,6 +32,6 @@ object SharedTabRobot : HomeRobot {
}
fun itemWithTextDisplayed(text: String) {
- filesListItems.filter(hasText(text)).assertCountEquals(1)
+ filesListItems.filter(node.withText(text)).assertCountEquals(1)
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt
index 3b5470ff..60c75b11 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/robot/WelcomeRobot.kt
@@ -18,33 +18,31 @@
package me.proton.android.drive.ui.robot
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.hasTestTag
-import androidx.compose.ui.test.onFirst
-import androidx.compose.ui.test.swipeLeft
-import androidx.compose.ui.test.swipeRight
+import me.proton.test.fusion.Fusion.node
+import me.proton.test.fusion.Fusion.allNodes
import me.proton.android.drive.ui.screen.WelcomeScreenTestTag
+import me.proton.test.fusion.ui.common.enums.SwipeDirection
import me.proton.core.drive.base.presentation.R as BasePresentation
import me.proton.core.presentation.R as CorePresentation
object WelcomeRobot : Robot {
- private val skipButton get() = nodes(hasTextResource(CorePresentation.string.presentation_skip)).onFirst()
- private val nextButton get() = nodes(hasTextResource(BasePresentation.string.common_next_action)).onFirst()
- private val getStartedButton get() = node(hasTextResource(BasePresentation.string.welcome_get_started_action))
- private val welcomeLabel get() = node(hasTextResource(BasePresentation.string.welcome_to_description))
- private val filesTitle get() = node(hasTextResource(BasePresentation.string.title_welcome_files))
- private val titleSharing get() = node(hasTextResource(BasePresentation.string.title_welcome_sharing))
- private val filesDescription get() = node(hasTextResource(BasePresentation.string.welcome_files_description))
- private val sharingDescription get() = node(hasTextResource(BasePresentation.string.welcome_sharing_description))
- private val welcomeScreen get() = node(hasTestTag(WelcomeScreenTestTag.screen))
+ private val skipButton get() = allNodes.withText(CorePresentation.string.presentation_skip).onFirst()
+ private val nextButton get() = allNodes.withText(BasePresentation.string.common_next_action).onFirst()
+ private val getStartedButton get() = node.withText(BasePresentation.string.welcome_get_started_action)
+ private val welcomeLabel get() = node.withText(BasePresentation.string.welcome_to_description)
+ private val filesTitle get() = node.withText(BasePresentation.string.title_welcome_files)
+ private val titleSharing get() = node.withText(BasePresentation.string.title_welcome_sharing)
+ private val filesDescription get() = node.withText(BasePresentation.string.welcome_files_description)
+ private val sharingDescription get() = node.withText(BasePresentation.string.welcome_sharing_description)
+ private val welcomeScreen get() = node.withTag(WelcomeScreenTestTag.screen)
- fun clickNext() = nextButton.tryToClickAndGoTo(this)
- fun clickSkip() = skipButton.tryToClickAndGoTo(FilesTabRobot)
- fun clickGetStarted() = getStartedButton.tryToClickAndGoTo(FilesTabRobot)
- fun swipeLeft() = welcomeScreen.tryPerformTouchInputAndGoTo(this) { this.swipeLeft() }
- fun swipeRight() = welcomeScreen.tryPerformTouchInputAndGoTo(this) { this.swipeRight() }
+ fun clickNext() = nextButton.clickTo(this)
+ fun clickSkip() = skipButton.clickTo(FilesTabRobot)
+ fun clickGetStarted() = getStartedButton.clickTo(FilesTabRobot)
+ fun swipeLeft() = apply { welcomeScreen.swipe(SwipeDirection.Left) }
+ fun swipeRight() = apply { welcomeScreen.swipe(SwipeDirection.Right) }
override fun robotDisplayed() {
- welcomeScreen.assertIsDisplayed()
+ welcomeScreen.await { assertIsDisplayed() }
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt
index 3e1488f3..abd27516 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/rules/LogoutAllRule.kt
@@ -20,13 +20,17 @@ package me.proton.android.drive.ui.rules
import kotlinx.coroutines.runBlocking
import me.proton.android.drive.ui.test.BaseTest.Companion.loginTestHelper
+import me.proton.android.drive.ui.test.BaseTest.Companion.quark
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class LogoutAllRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
- runBlocking { loginTestHelper.logoutAll() }
+ runBlocking {
+ quark.jailUnban()
+ loginTestHelper.logoutAll()
+ }
return base
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt
index 824d85d2..d5f50904 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/BaseTest.kt
@@ -19,26 +19,25 @@
package me.proton.android.drive.ui.test
import android.app.Application
-import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import dagger.hilt.android.EntryPointAccessors
import me.proton.android.drive.test.BuildConfig
import me.proton.android.drive.ui.MainActivity
import me.proton.android.drive.ui.robot.Robot
import me.proton.android.drive.ui.rules.LogoutAllRule
+import me.proton.android.drive.ui.toolkits.screenshot
import me.proton.core.auth.presentation.testing.ProtonTestEntryPoint
import me.proton.core.test.quark.Quark
import me.proton.core.test.quark.data.User
import me.proton.core.util.kotlin.deserialize
import me.proton.core.util.kotlin.deserializeList
+import me.proton.test.fusion.FusionConfig
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestName
-
-typealias AndroidComposeRule = AndroidComposeTestRule, MainActivity>
+import java.util.concurrent.atomic.AtomicInteger
open class BaseTest {
@Rule
@@ -46,17 +45,19 @@ open class BaseTest {
val ruleChain: RuleChain = RuleChain.outerRule(testName)
@get:Rule
- val composeTestRule: AndroidComposeRule = createAndroidComposeRule()
+ val composeTestRule = createAndroidComposeRule()
@get:Rule(order = 0)
val logoutAllRule = LogoutAllRule()
init {
- setGlobalComposeRule(composeTestRule)
+ FusionConfig.Compose.testRule.set(composeTestRule)
+ FusionConfig.Compose.useUnmergedTree.set(true)
+ FusionConfig.Compose.onFailure = { screenshot() }
+ screenshotCounter.set(0)
}
- inline fun T. verify(crossinline block: T.() -> Any): T =
- apply { waitFor(this) { block() } }
+ inline fun T.verify(crossinline block: T.() -> Any): T = apply { block() }
companion object {
private val protonTestEntryPoint by lazy {
@@ -73,17 +74,11 @@ open class BaseTest {
)
}
- private var globalComposeRule: AndroidComposeRule? = null
-
val loginTestHelper by lazy { protonTestEntryPoint.loginTestHelper }
val uiTestHelper by lazy { uiTestEntryPoint.uiTestHelper }
val testName = TestName()
- val screenshotLocation: String get() = "/sdcard/Pictures"
- val composeTestRule: AndroidComposeRule
- get() = globalComposeRule
- ?: throw AssertionError(
- "Compose test rule was not set. Make sure to call setGlobalComposeRule(rule) first"
- )
+ val screenshotLocation get() = "/sdcard/Pictures/Screenshots/${testName.methodName}/"
+ val screenshotCounter = AtomicInteger(0)
// TODO: before publishing to github, this information should be moved from assets into gitlab vars
val users = User.Users(InstrumentationRegistry.getInstrumentation().context
@@ -102,9 +97,5 @@ open class BaseTest {
.bufferedReader()
.use { it.readText() }
.deserialize())
-
- fun setGlobalComposeRule(rule: AndroidComposeRule) {
- globalComposeRule = rule
- }
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt
index 7aec11a1..db787b96 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowErrorTest.kt
@@ -35,7 +35,8 @@ import org.junit.runners.Parameterized
class CreatingFolderFlowErrorTest(
private val folderName: String,
private val errorMessage: String,
-): BaseTest() {
+ private val friendlyName: String
+) : BaseTest() {
private val user
get() = User(
@@ -64,11 +65,15 @@ class CreatingFolderFlowErrorTest(
}
companion object {
- @get:Parameterized.Parameters(name = "folderName={0}, errorMessage={2}")
+ @get:Parameterized.Parameters(name = "{2}")
@get:JvmStatic
val data = listOf(
- arrayOf("folder1", "A file or folder with that name already exists"),
- arrayOf(getRandomString(256), StringUtils.stringFromResource(R.string.common_error_name_too_long, 255)),
+ arrayOf("folder1", "A file or folder with that name already exists", "alreadyExists"),
+ arrayOf(
+ getRandomString(256),
+ StringUtils.stringFromResource(R.string.common_error_name_too_long, 255),
+ "tooLongFilename"
+ ),
)
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt
index 8145ddc6..bfdb1a52 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/CreatingFolderFlowSuccessTest.kt
@@ -53,9 +53,12 @@ class CreatingFolderFlowSuccessTest : BaseTest() {
val newFolderName = getRandomString()
FilesTabRobot
- .swipeUpToItemWithName(subFolderName)
+ .scrollToItemWithName(subFolderName)
.clickMoreOnFolder(subFolderName)
.clickMove()
+ .verify {
+ robotDisplayed()
+ }
.clickAddFolder()
.typeFolderName(newFolderName)
.clickCreate()
@@ -63,10 +66,7 @@ class CreatingFolderFlowSuccessTest : BaseTest() {
CreateFolderRobot
.dismissSuccessGrowler(newFolderName, MoveToFolderRobot)
.verify {
- // Workaround (possibly a bug?)
- // For some reason does not auto swipe to top on emulators only
- swipeDown()
- itemListWithTextDisplayed(newFolderName)
+ itemWithTextDisplayed(newFolderName)
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt
index 4ff26b3c..476c2a5f 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFileSuccessFlowTest.kt
@@ -54,7 +54,7 @@ class RenamingFileSuccessFlowTest: BaseTest() {
val newName = "picture.jpg"
FilesTabRobot
- .swipeUpToItemWithName(oldName)
+ .scrollToItemWithName(oldName)
.clickOnFile(oldName)
.verify {
robotDisplayed()
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt
index d7179faa..c086ee2b 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFlowSuccessTest.kt
@@ -50,7 +50,7 @@ class RenamingFlowSuccessTest(
@Test
fun renameSuccess() {
FilesTabRobot
- .swipeUpToItemWithName(itemToBeRenamed)
+ .scrollToItemWithName(itemToBeRenamed)
.clickMoreOnItem(itemToBeRenamed)
.clickRename()
.clearName()
@@ -62,7 +62,7 @@ class RenamingFlowSuccessTest(
}
companion object {
- @get:Parameterized.Parameters(name = "folderToBeRenamed={0}, newFolderName={1}")
+ @get:Parameterized.Parameters(name = "folderToBeRenamed={0}_newFolderName={1}")
@get:JvmStatic
val data = listOf(
arrayOf("folder1", "...folder1"),
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt
index cea09cec..cc30ba72 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/test/flow/RenamingFolderFlowErrorTest.kt
@@ -36,7 +36,8 @@ class RenamingFolderFlowErrorTest(
private val itemToBeRenamed: String,
private val newItemName: String,
private val errorMessage: String,
-): BaseTest() {
+ private val friendlyName: String
+) : BaseTest() {
private val user
get() = User(
@@ -53,7 +54,7 @@ class RenamingFolderFlowErrorTest(
@Test
fun renameError() {
FilesTabRobot
- .swipeUpToItemWithName(itemToBeRenamed)
+ .scrollToItemWithName(itemToBeRenamed)
.clickMoreOnFolder(itemToBeRenamed)
.clickRename()
.clearName()
@@ -65,13 +66,33 @@ class RenamingFolderFlowErrorTest(
}
companion object {
- @get:Parameterized.Parameters(name = "folderToBeRenamed={0}, newFolderName={1}, errorMessage={2}")
+ @get:Parameterized.Parameters(name = "{3}")
@get:JvmStatic
val data = listOf(
- arrayOf("folder1", "folder2", "An item with that name already exists in current folder"),
- arrayOf("folder1", "", StringUtils.stringFromResource(R.string.common_error_name_is_blank)),
- arrayOf("folder1", getRandomString(256), StringUtils.stringFromResource(R.string.common_error_name_too_long, 255)),
- arrayOf("folder2", "folder1", "An item with that name already exists in current folder"),
+ arrayOf(
+ "folder1",
+ "folder2",
+ "An item with that name already exists in current folder",
+ "Existing folder"
+ ),
+ arrayOf(
+ "folder1",
+ "",
+ StringUtils.stringFromResource(R.string.common_error_name_is_blank),
+ "Empty folder name"
+ ),
+ arrayOf(
+ "folder1",
+ getRandomString(256),
+ StringUtils.stringFromResource(R.string.common_error_name_too_long, 255),
+ "Very long name"
+ ),
+ arrayOf(
+ "folder2",
+ "folder1",
+ "An item with that name already exists in current folder",
+ "Existing folder 2"
+ )
)
}
}
diff --git a/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt b/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt
index 27e4d781..35219739 100644
--- a/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt
+++ b/app/src/uiTest/kotlin/me/proton/android/drive/ui/toolkits/Helpers.kt
@@ -18,9 +18,56 @@
package me.proton.android.drive.ui.toolkits
+import android.graphics.Bitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.onFirst
+import androidx.test.platform.app.InstrumentationRegistry
+import me.proton.android.drive.ui.test.BaseTest
+import me.proton.core.test.android.instrumented.ProtonTest
+import me.proton.core.util.kotlin.CoreLogger
+import me.proton.test.fusion.FusionConfig
+import java.io.FileNotFoundException
+import java.io.FileOutputStream
+
fun getRandomString(length: Int = 10): String {
val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9').shuffled()
return (1..length)
.map { allowedChars.random() }
.joinToString("")
}
+
+fun BaseTest.Companion.screenshot() {
+ val screenshotNumber = screenshotCounter.getAndIncrement()
+ val fileName = "${screenshotLocation}/${screenshotNumber}_${testName.methodName}.png"
+
+ if (screenshotNumber == 0) {
+ InstrumentationRegistry
+ .getInstrumentation()
+ .uiAutomation
+ .executeShellCommand("mkdir -p $screenshotLocation")
+ }
+
+ try {
+ FusionConfig.Compose.testRule.get()
+ .onAllNodes(SemanticsMatcher("isRoot") { it.isRoot })
+ .onFirst()
+ .captureToImage()
+ .asAndroidBitmap()
+ .let { bitmap ->
+ FileOutputStream(fileName).use {
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
+ }
+ }
+
+ } catch (throwable: Throwable) {
+ when (throwable) {
+ is FileNotFoundException -> "File not found"
+ is OutOfMemoryError -> "Out of memory"
+ else -> "Unknown error"
+ }.let {
+ CoreLogger.e(ProtonTest.testTag, throwable, "Could not take screenshot: $it")
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt
index b8c1bb04..343de68a 100644
--- a/buildSrc/src/main/kotlin/Config.kt
+++ b/buildSrc/src/main/kotlin/Config.kt
@@ -22,7 +22,7 @@ object Config {
const val minSdk = 23
const val targetSdk = 33
const val testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- const val versionName = "1.0.4"
+ const val versionName = "1.1.0"
const val archivesBaseName = "ProtonDrive-$versionName"
val resourceConfigurations = listOf("en")
}
diff --git a/buildSrc/src/main/kotlin/DeleteTestPlugin.kt b/buildSrc/src/main/kotlin/DeleteTestPlugin.kt
index 6f4dd797..3abbdd76 100644
--- a/buildSrc/src/main/kotlin/DeleteTestPlugin.kt
+++ b/buildSrc/src/main/kotlin/DeleteTestPlugin.kt
@@ -1,7 +1,3 @@
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.api.tasks.Delete
-
/*
* Copyright (c) 2023 Proton AG.
* This file is part of Proton Drive.
@@ -20,6 +16,11 @@ import org.gradle.api.tasks.Delete
* along with Proton Drive. If not, see .
*/
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.tasks.Delete
+
+
class DeleteTestPlugin : Plugin {
override fun apply(project: Project) {
@@ -33,4 +34,4 @@ class DeleteTestPlugin : Plugin {
)
}
}
-}
\ No newline at end of file
+}
diff --git a/buildSrc/src/main/kotlin/DriveModule.kt b/buildSrc/src/main/kotlin/DriveModule.kt
index 23723e02..df0f8851 100644
--- a/buildSrc/src/main/kotlin/DriveModule.kt
+++ b/buildSrc/src/main/kotlin/DriveModule.kt
@@ -100,6 +100,15 @@ fun Project.driveModule(
extensions.findByType()?.apply {
compileSdk = Config.compileSdk
+
+ defaultConfig {
+ javaCompileOptions {
+ annotationProcessorOptions {
+ arguments["room.schemaLocation"] = "$projectDir/schemas"
+ }
+ }
+ }
+
buildTypes {
debug {
enableUnitTestCoverage = true
@@ -189,7 +198,7 @@ fun Project.driveModule(
}
if (includeSubmodules) {
val fullName = project.fullName
- projectDir.findModules().forEach { module ->
+ projectDir.findModules().filterNot { it.endsWith("-test") }.forEach { module ->
add("api", project(":$fullName$module"))
}
}
@@ -202,7 +211,7 @@ fun Project.driveModule(
}
// first alpha and fourth beta were not tagged in git so we add them to list of all git tags
-val Project.tags get() = "1.0.0-alpha01\n1.0.0_cancelled(16)\n1.0.0_cancelled(18)\n1.0.0_cancelled(20)\n1.0.0-beta04\n" + "git tag".runCommand(workingDir = rootDir)
+val Project.tags get() = "1.0.0-alpha01\n1.0.0_cancelled(16)\n1.0.0_cancelled(18)\n1.0.0_cancelled(20)\n1.0.0-beta04\n1.0.3_iap(26)\n" + "git tag".runCommand(workingDir = rootDir)
val Project.versionCodeFromTags: Int get() = tags.countSubstrings("\n") + 2 // last new line + next tag
diff --git a/drive/base/data-test/build.gradle.kts b/drive/base/data-test/build.gradle.kts
new file mode 100644
index 00000000..ab13ea8f
--- /dev/null
+++ b/drive/base/data-test/build.gradle.kts
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+plugins {
+ id("com.android.library")
+}
+
+driveModule(
+ hilt = true,
+) {
+ api(libs.core.domain)
+}
+
+configureJacoco()
\ No newline at end of file
diff --git a/drive/base/data-test/src/main/AndroidManifest.xml b/drive/base/data-test/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..d88b8a21
--- /dev/null
+++ b/drive/base/data-test/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt b/drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt
new file mode 100644
index 00000000..8628766d
--- /dev/null
+++ b/drive/base/data-test/src/main/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManager.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.base.data.test.manager
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.domain.arch.onSuccess
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class StubbedWorkManager @Inject constructor() {
+
+ var behavior: () -> DataResult = BEHAVIOR_SUCCESS
+
+ data class Work(
+ val name: String,
+ val data: List
+ )
+
+ val works = MutableStateFlow(listOf())
+
+ fun add(name: String, vararg data: Any): DataResult = add(Work(name, listOf(*data)))
+
+ private fun add(work: Work) = behavior().onSuccess {
+ works.value = works.value + work
+ }
+
+ fun execute() {
+ works.value = emptyList()
+ }
+
+ companion object {
+ val BEHAVIOR_SUCCESS = { DataResult.Success(ResponseSource.Local, "") }
+ val BEHAVIOR_ERROR = { DataResult.Error.Local("behavior_error", null) }
+ }
+}
+
+fun StubbedWorkManager.assertHasWorks(name: String) {
+ if (works.value.none { it.name == name }) {
+ throw AssertionError("Does not contains work: $name in ${works.value.map { it.name }}")
+ }
+}
+
+fun StubbedWorkManager.assertHasWork(name: String, vararg data: Any) {
+ val namedWorks = works.value.filter { it.name == name }
+ if (namedWorks.isEmpty()) {
+ throw AssertionError("Does not contains work: $name in ${works.value.map { it.name }}")
+ }
+ val dataAsList = listOf(*data)
+ val dataWorks = namedWorks.filter { it.data == dataAsList }
+ if (dataWorks.isEmpty()) {
+ throw AssertionError("Does not contains work: $name with the same data, expected:$dataAsList but was: ${namedWorks.map { it.data }}")
+ } else if (dataWorks.size > 1) {
+ throw AssertionError("Does contains more then one work: $name with data: $dataAsList")
+ }
+}
diff --git a/drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt b/drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt
new file mode 100644
index 00000000..052f4690
--- /dev/null
+++ b/drive/base/data-test/src/test/kotlin/me/proton/core/drive/base/data/test/manager/StubbedWorkManagerTest.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.base.data.test.manager
+
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.arch.ResponseSource
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
+import org.junit.Test
+
+class StubbedWorkManagerTest {
+ private val manager = StubbedWorkManager()
+
+ @Test
+ fun `Given success behavior When add work without data Then returns success and succeed asserting`() {
+ manager.behavior = StubbedWorkManager.BEHAVIOR_SUCCESS
+
+ val result = manager.add("name")
+
+ assertEquals(DataResult.Success(ResponseSource.Local, ""), result)
+ manager.assertHasWorks("name")
+ }
+
+ @Test
+ fun `Given success behavior When add work with data Then returns success and succeed asserting`() {
+ manager.behavior = StubbedWorkManager.BEHAVIOR_SUCCESS
+
+ val result = manager.add("name", "data")
+
+ assertEquals(DataResult.Success(ResponseSource.Local, ""), result)
+ manager.assertHasWork("name", "data")
+ }
+
+ @Test
+ fun `Given error behavior When add work Then returns error and fail asserting`() {
+ manager.behavior = StubbedWorkManager.BEHAVIOR_ERROR
+
+ val result = manager.add("name")
+
+ assertEquals(DataResult.Error.Local("behavior_error", null), result)
+ assertThrows(AssertionError::class.java) {
+ manager.assertHasWorks("name")
+ }
+ }
+
+ @Test
+ fun `Given a work When execute Then have no work`() {
+ manager.add("name")
+
+ manager.execute()
+
+ assertEquals(emptyList(), manager.works.value)
+ }
+
+ @Test
+ fun `Given no work When asserting for works Then fail`() {
+ val exception = assertThrows(AssertionError::class.java) {
+ manager.assertHasWorks("name")
+ }
+ assertEquals(
+ "Does not contains work: name in []",
+ exception.message
+ )
+ }
+
+ @Test
+ fun `Given work with different name When asserting for work Then fail`() {
+ manager.add("different-name")
+
+ val exception = assertThrows(AssertionError::class.java) {
+ manager.assertHasWork("name")
+ }
+
+ assertEquals(
+ "Does not contains work: name in [different-name]",
+ exception.message
+ )
+ }
+
+ @Test
+ fun `Given two same works When asserting for one work Then fail`() {
+ manager.add("name", "data")
+ manager.add("name", "data")
+
+ val exception = assertThrows(AssertionError::class.java) {
+ manager.assertHasWork("name", "data")
+ }
+
+ assertEquals(
+ "Does contains more then one work: name with data: [data]",
+ exception.message
+ )
+ }
+
+ @Test
+ fun `Given two same works with different data When asserting for one work Then fail`() {
+ manager.add("name", "data1")
+ manager.add("name", "data2")
+
+ manager.assertHasWork("name", "data1")
+ }
+
+ @Test
+ fun `Given work with different data When asserting for work Then fail`() {
+ manager.add("name", "different-data")
+
+ val exception = assertThrows(AssertionError::class.java) {
+ manager.assertHasWork("name", "data")
+ }
+
+ assertEquals(
+ "Does not contains work: name with the same data, expected:[data] but was: [[different-data]]",
+ exception.message
+ )
+ }
+}
\ No newline at end of file
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt
index 816c9f18..f351b120 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/api/RunCatchingApiException.kt
@@ -17,15 +17,15 @@
*/
package me.proton.core.drive.base.data.api
-import me.proton.core.data.arch.toDataResult
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.drive.base.domain.extension.toDataResult
import me.proton.core.network.domain.ApiException
inline fun runCatchingApiException(block: () -> T): DataResult {
return try {
DataResult.Success(ResponseSource.Remote, block())
} catch (e: ApiException) {
- e.error.toDataResult()
+ e.toDataResult()
}
}
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt
index 017d73d1..6e07fbfe 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/Column.kt
@@ -27,6 +27,8 @@ object Column {
const val CONTENT_KEY_PACKET_SIGNATURE = "content_key_packet_signature"
const val CREATION_TIME = "creation_time"
const val CREATOR_EMAIL = "creatior_email"
+ const val DIGESTS = "digests"
+ const val DURATION = "duration"
const val ENCRYPTED = "encrypted"
const val ENCRYPTED_NAME = "encrypted_name"
const val ENCRYPTED_SIGNATURE = "encrypted_signature"
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt
index 1331403f..12fa8181 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/db/paging/Flow.kt
@@ -22,19 +22,24 @@ import androidx.paging.PagingSource
import androidx.paging.PagingState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.flow.transformLatest
import me.proton.core.drive.base.domain.extension.mapWithPrevious
import me.proton.core.drive.base.domain.log.LogTag
import me.proton.core.util.kotlin.CoreLogger
import kotlin.coroutines.CoroutineContext
+import kotlin.math.ceil
/**
* When using Room's [PagingSource] implementation, if the tables listened on are being updated
@@ -74,7 +79,7 @@ fun Flow>>.asPagingSource(
val pageList = list
.onFailure { throwable ->
val error = throwable.cause ?: throwable
- CoreLogger.d(LogTag.PAGING, "load (key=$pageKey) from flow failed with $error")
+ CoreLogger.d(LogTag.PAGING, throwable, "load (key=$pageKey) from flow failed with $error")
return LoadResult.Error(error)
}
.getOrThrow()
@@ -93,7 +98,7 @@ fun Flow>>.asPagingSource(
nextKey = nextKey,
)
} catch (e: Throwable) {
- CoreLogger.d(LogTag.PAGING, "load (key=$pageKey) from flow failed with ${e.cause ?: e}")
+ CoreLogger.d(LogTag.PAGING, e, "load (key=$pageKey) from flow failed with ${e.cause ?: e}")
LoadResult.Invalid()
}
}
@@ -103,3 +108,117 @@ object PagingSourceScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + Job()
}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+fun ((fromIndex: Int, count: Int) -> Flow>>).asPagingSource(
+ sourceSize: Flow,
+ observablePageSize: Int,
+ stopOnFailure: Boolean = true,
+ processPage: (suspend (List) -> List)? = null,
+): PagingSource =
+ object : PagingSource() {
+
+ private val itemsCount: StateFlow = sourceSize
+ .takeWhile { invalid.not() }
+ .distinctUntilChanged()
+ .mapWithPrevious { previous, current ->
+ if (previous != null) invalidate()
+ current
+ }
+ .stateIn(PagingSourceScope, SharingStarted.Eagerly, null)
+
+ private val fromIndex = MutableStateFlow(null)
+
+ private val listFlow: StateFlow>?> = fromIndex
+ .takeWhile { invalid.not() }
+ .filterNotNull()
+ .transformLatest { fromIndex ->
+ emitAll(
+ this@asPagingSource(fromIndex, observablePageSize)
+ .takeWhile { invalid.not() }
+ .distinctUntilChanged()
+ .mapWithPrevious { previous, current ->
+ if (previous != null && !(previous.isFailure && stopOnFailure)) {
+ invalidate()
+ }
+ current
+ }
+ )
+ }
+ .stateIn(PagingSourceScope, SharingStarted.Eagerly, null)
+
+ override fun getRefreshKey(state: PagingState): Int? =
+ state.anchorPosition?.let { anchorPosition ->
+ state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
+ ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
+ }
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ require(observablePageSize >= params.loadSize * 4) {
+ "Observable page size ($observablePageSize) must be at least 4 times as big as load page size (${params.loadSize})"
+ }
+ val pageKey = params.key ?: 0
+ val items = itemsCount.filterNotNull().first()
+ val pageRange = rangeFromPage(pageKey, params.loadSize, items)
+ val currentIndex = fromIndex.value ?: findIndexForRange(pageRange, observablePageSize, items)
+ fromIndex.value = currentIndex
+ val list = listFlow.filterNotNull().first()
+ return try {
+ val pageList = list
+ .onFailure { throwable ->
+ val error = throwable.cause ?: throwable
+ CoreLogger.d(LogTag.PAGING, throwable, "load (key=$pageKey) from flow failed with $error")
+ return LoadResult.Error(error)
+ }
+ .getOrThrow()
+ val page = pageList.subList(pageRange.first - currentIndex, pageRange.last - currentIndex + 1)
+ val prevKey = (pageKey - 1).takeIf { key -> key >= 0 }
+ val nextKey = (pageKey + 1).takeIf { key -> key < ceil(items / params.loadSize.toDouble()).toInt() }
+ CoreLogger.d(
+ tag = LogTag.PAGING,
+ message = "load (key=$pageKey, items=${page.size}) from flow (items=${pageList.size}) nextKey=$nextKey prevKey=$prevKey"
+ )
+ LoadResult.Page(
+ data = processPage?.invoke(page) ?: page,
+ prevKey = prevKey,
+ nextKey = nextKey,
+ ).also {
+ val rangeDirection = rangeDirection(items, currentIndex, observablePageSize, pageRange)
+ if (rangeDirection != RangeDirection.CURRENT) {
+ invalidate()
+ }
+ }
+ } catch (e: Throwable) {
+ CoreLogger.d(LogTag.PAGING, e, "load (key=$pageKey) from flow failed with ${e.cause ?: e}")
+ LoadResult.Invalid()
+ }
+ }
+
+ private fun rangeFromPage(pageIndex: Int, pageSize: Int, itemsCount: Int): IntRange =
+ IntRange(pageIndex * pageSize, minOf(((pageIndex + 1) * pageSize) - 1, itemsCount - 1))
+
+ private fun rangeDirection(itemsCount: Int, fromIndex: Int, observablePageSize: Int, pageRange: IntRange): RangeDirection {
+ val hasPrevious = fromIndex > 0
+ val hasNext = fromIndex + observablePageSize < itemsCount
+ val pageSize = pageRange.last - pageRange.first + 1
+ return when {
+ !hasPrevious && !hasNext -> RangeDirection.CURRENT
+ hasPrevious && pageRange.first - pageSize <= fromIndex -> RangeDirection.PREVIOUS
+ hasNext && pageRange.last + pageSize >= fromIndex + observablePageSize -> RangeDirection.NEXT
+ else -> RangeDirection.CURRENT
+ }
+ }
+
+ private fun findIndexForRange(pageRange: IntRange, observablePageSize: Int, itemsCount: Int): Int = when {
+ itemsCount <= observablePageSize -> 0
+ pageRange.first <= observablePageSize / 2 -> 0
+ itemsCount - pageRange.last <= observablePageSize / 2 -> itemsCount - observablePageSize
+ else -> pageRange.first - observablePageSize / 2
+ }
+ }
+
+private enum class RangeDirection {
+ CURRENT,
+ PREVIOUS,
+ NEXT,
+}
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt
index d44a22e5..5ce3bda0 100644
--- a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/di/BaseBindModule.kt
@@ -21,17 +21,13 @@ import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
-import dagger.multibindings.IntoSet
import me.proton.core.drive.base.data.formatter.DateTimeFormatterImpl
-import me.proton.core.drive.base.data.provider.ExifImageResolutionProvider
-import me.proton.core.drive.base.data.provider.MetadataRetrieverVideoResolutionProvider
-import me.proton.core.drive.base.data.provider.MimeTypeProviderImpl
import me.proton.core.drive.base.data.usecase.CopyToClipboardImpl
+import me.proton.core.drive.base.data.usecase.GetMemoryInfoImpl
import me.proton.core.drive.base.data.usecase.Sha256Impl
import me.proton.core.drive.base.domain.formatter.DateTimeFormatter
-import me.proton.core.drive.base.domain.provider.MediaResolutionProvider
-import me.proton.core.drive.base.domain.provider.MimeTypeProvider
import me.proton.core.drive.base.domain.usecase.CopyToClipboard
+import me.proton.core.drive.base.domain.usecase.GetMemoryInfo
import me.proton.core.drive.base.domain.usecase.Sha256
import javax.inject.Singleton
@@ -50,4 +46,8 @@ interface BaseBindModule {
@Binds
@Singleton
fun bindsSha256Impl(impl: Sha256Impl): Sha256
+
+ @Binds
+ @Singleton
+ fun bindsGetMemoryInfoImpl(impl: GetMemoryInfoImpl): GetMemoryInfo
}
diff --git a/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt
new file mode 100644
index 00000000..b0707893
--- /dev/null
+++ b/drive/base/data/src/main/kotlin/me/proton/core/drive/base/data/usecase/GetMemoryInfoImpl.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.base.data.usecase
+
+import android.app.ActivityManager
+import me.proton.core.drive.base.domain.entity.MemoryInfo
+import me.proton.core.drive.base.domain.extension.bytes
+import me.proton.core.drive.base.domain.usecase.GetMemoryInfo
+import javax.inject.Inject
+
+class GetMemoryInfoImpl @Inject constructor(
+ private val activityManager: ActivityManager,
+) : GetMemoryInfo {
+ override operator fun invoke(): Result = runCatching {
+ with(
+ ActivityManager.MemoryInfo().also { memoryInfo ->
+ activityManager.getMemoryInfo(memoryInfo)
+ }
+ ) {
+ MemoryInfo(
+ isLowOnMemory = lowMemory,
+ memoryClass = (activityManager.memoryClass * 1024 * 2024).bytes
+ )
+ }
+ }
+}
diff --git a/drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt b/drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt
new file mode 100644
index 00000000..cf6a0b15
--- /dev/null
+++ b/drive/base/data/src/test/kotlin/me/proton/core/drive/base/data/db/paging/FlowTest.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.base.data.db.paging
+
+import androidx.paging.PagingSource
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runTest
+import me.proton.core.test.kotlin.assertEquals
+import org.junit.Assert
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class PagingSourceTest {
+ private val backingData = listOf(
+ "first",
+ "second",
+ "third",
+ "fourth",
+ "fifth",
+ "sixth",
+ "seventh",
+ "eighth",
+ "ninth",
+ "tenth",
+ "eleventh",
+ "twelfth",
+ "thirteenth",
+ "fourteenth",
+ "fifteenth",
+ )
+ private val source = MutableStateFlow(backingData)
+ private fun flowFromBackingData(fromIndex: Int, count: Int): Flow>> = flow {
+ val source = this@PagingSourceTest.source.value
+ emit(
+ when {
+ fromIndex < 0 || fromIndex >= source.count() -> Result.failure(IllegalArgumentException())
+ else -> Result.success(source.subList(fromIndex, (fromIndex + count).coerceIn(0, source.size)))
+ }
+ )
+ }
+ private fun pagingSource(observablePageSize: Int = DEFAULT_OBSERVABLE_PAGE_SIZE_S) = { fromIndex: Int, count: Int ->
+ flowFromBackingData(fromIndex, count)
+ }.asPagingSource(
+ sourceSize = source.map { backingData -> backingData.size },
+ observablePageSize = observablePageSize,
+ )
+
+ @Test
+ fun `refresh paging list when observable page size is bigger then underlying data`() = runTest {
+ val pagingSource = pagingSource(maxOf(DEFAULT_OBSERVABLE_PAGE_SIZE_L, backingData.size))
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("first", "second", "third"),
+ prevKey = null,
+ nextKey = 1,
+ ),
+ actual = pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = LOAD_SIZE_M,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("thirteenth", "fourteenth", "fifteenth"),
+ prevKey = 1,
+ nextKey = null,
+ ),
+ actual = pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = 2,
+ loadSize = LOAD_SIZE_L,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ }
+
+ @Test
+ fun `refresh paging list`() = runTest {
+ val pagingSource = pagingSource(DEFAULT_OBSERVABLE_PAGE_SIZE_M)
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("first", "second"),
+ prevKey = null,
+ nextKey = 1,
+ ),
+ actual = pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = LOAD_SIZE_S,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("first", "second", "third"),
+ prevKey = null,
+ nextKey = 1,
+ ),
+ actual = pagingSource.load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = LOAD_SIZE_M,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ }
+
+ @Test
+ fun `append list with second page`() = runTest {
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("third", "fourth"),
+ prevKey = 0,
+ nextKey = 2,
+ ),
+ actual = pagingSource().load(
+ PagingSource.LoadParams.Append(
+ key = 1,
+ loadSize = LOAD_SIZE_S,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ }
+
+ @Test
+ fun `append middle pages`() = runTest {
+ val pagingSource = pagingSource(DEFAULT_OBSERVABLE_PAGE_SIZE_M)
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("seventh", "eighth", "ninth"),
+ prevKey = 1,
+ nextKey = 3,
+ ),
+ actual = pagingSource.load(
+ PagingSource.LoadParams.Append(
+ key = 2,
+ loadSize = LOAD_SIZE_M,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("tenth", "eleventh", "twelfth"),
+ prevKey = 2,
+ nextKey = 4,
+ ),
+ actual = pagingSource.load(
+ PagingSource.LoadParams.Append(
+ key = 3,
+ loadSize = LOAD_SIZE_M,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ }
+
+ @Test
+ fun `append list with last page`() = runTest {
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("fifteenth"),
+ prevKey = 6,
+ nextKey = null,
+ ),
+ actual = pagingSource().load(
+ PagingSource.LoadParams.Append(
+ key = 7,
+ loadSize = LOAD_SIZE_S,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ }
+
+ @Test
+ fun `prepend list with third page`() = runTest {
+ assertEquals(
+ expected = PagingSource.LoadResult.Page(
+ data = listOf("fifth", "sixth"),
+ prevKey = 1,
+ nextKey = 3,
+ ),
+ actual = pagingSource().load(
+ PagingSource.LoadParams.Prepend(
+ key = 2,
+ loadSize = LOAD_SIZE_S,
+ placeholdersEnabled = false,
+ )
+ ),
+ message = { "" },
+ )
+ }
+
+ @Test
+ fun `load page size must be less then observablePageSize`() {
+ Assert.assertThrows(IllegalArgumentException::class.java) {
+ runTest {
+ pagingSource(DEFAULT_OBSERVABLE_PAGE_SIZE_S).load(
+ PagingSource.LoadParams.Refresh(
+ key = null,
+ loadSize = DEFAULT_OBSERVABLE_PAGE_SIZE_S,
+ placeholdersEnabled = false,
+ )
+ )
+ }
+ }
+ }
+
+ companion object {
+ const val LOAD_SIZE_S = 2
+ const val LOAD_SIZE_M = 3
+ const val LOAD_SIZE_L = 6
+ const val DEFAULT_OBSERVABLE_PAGE_SIZE_S = LOAD_SIZE_S * 4
+ const val DEFAULT_OBSERVABLE_PAGE_SIZE_M = LOAD_SIZE_M * 4
+ const val DEFAULT_OBSERVABLE_PAGE_SIZE_L = LOAD_SIZE_L * 4
+ }
+}
diff --git a/drive/base/domain/build.gradle.kts b/drive/base/domain/build.gradle.kts
index 92251e83..59f67c2c 100644
--- a/drive/base/domain/build.gradle.kts
+++ b/drive/base/domain/build.gradle.kts
@@ -26,12 +26,12 @@ driveModule(
api(project(":drive:message-queue:domain"))
api(libs.core.cryptoCommon)
api(libs.core.domain)
- api(libs.core.user)
+ api(libs.core.user.domain)
- implementation(libs.core.accountManager)
+ implementation(libs.core.accountManager.domain)
implementation(libs.core.data)
- implementation(libs.core.key)
- implementation(libs.core.network)
+ implementation(libs.core.key.domain)
+ implementation(libs.core.network.domain)
}
configureJacoco()
diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt
new file mode 100644
index 00000000..ac0a877f
--- /dev/null
+++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/entity/MemoryInfo.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.base.domain.entity
+
+data class MemoryInfo(
+ val isLowOnMemory: Boolean,
+ val memoryClass: Bytes,
+)
diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt
new file mode 100644
index 00000000..a5932e33
--- /dev/null
+++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/ApiException.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.base.domain.extension
+
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.network.domain.ApiException
+import me.proton.core.network.domain.ApiResult
+import me.proton.core.util.kotlin.exhaustive
+
+fun ApiException.toDataResult() : DataResult.Error = when (val e = error) {
+ is ApiResult.Error.Http -> {
+ DataResult.Error.Remote(
+ message = e.proton?.error ?: message,
+ cause = this,
+ protonCode = e.proton?.code ?: 0,
+ httpCode = e.httpCode
+ )
+ }
+ is ApiResult.Error.Parse -> DataResult.Error.Remote(cause?.message, this)
+ is ApiResult.Error.Connection -> DataResult.Error.Remote(cause?.message, this)
+}.exhaustive
diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt
index 98d36e6b..ef3631b7 100644
--- a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt
+++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/extension/Bytes.kt
@@ -20,6 +20,7 @@ package me.proton.core.drive.base.domain.extension
import me.proton.core.drive.base.domain.entity.Bytes
import java.io.File
+inline val Int.GiB: Bytes get() = (this * 1_073_741_824L).bytes
inline val Int.MiB: Bytes get() = (this * 1_048_576L).bytes
inline val Int.KiB: Bytes get() = (this * 1_024L).bytes
diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt
index 4eed3f71..6011ecc9 100644
--- a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt
+++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/provider/ConfigurationProvider.kt
@@ -23,6 +23,8 @@ import me.proton.core.drive.base.domain.extension.MiB
import me.proton.core.drive.base.domain.extension.bytes
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
interface ConfigurationProvider {
val host: String
@@ -30,6 +32,7 @@ interface ConfigurationProvider {
val appVersionHeader: String
val uiPageSize: Int get() = 50
val apiPageSize: Int get() = 150
+ val dbPageSize: Int get() = 500
val cacheMaxEntries: Int get() = 10_000
val linkMaxNameLength: Int get() = 255
val blockMaxSize: Bytes get() = 4.MiB
@@ -49,4 +52,8 @@ interface ConfigurationProvider {
val validateUploadLimit: Boolean get() = true
val uploadLimitThreshold: Int get() = 250
val useExceptionMessage: Boolean get() = false
+ val digestAlgorithms: List get() = listOf("SHA1")
+ val autoLockDurations: Set get() = setOf(
+ 0.seconds, 60.seconds, 2.minutes, 5.minutes, 15.minutes, 30.minutes
+ )
}
diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt
index 649e5668..af28a865 100644
--- a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt
+++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/repository/Fetcher.kt
@@ -19,9 +19,9 @@ package me.proton.core.drive.base.domain.repository
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.FlowCollector
-import me.proton.core.data.arch.toDataResult
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.drive.base.domain.extension.toDataResult
import me.proton.core.network.domain.ApiException
suspend inline fun FlowCollector>.fetcher(fetchAction: () -> Unit) {
@@ -31,7 +31,7 @@ suspend inline fun FlowCollector>.fetcher(fetchAction: () -> U
} catch (e: CancellationException) {
throw e
} catch (e: ApiException) {
- emit(e.error.toDataResult())
+ emit(e.toDataResult())
} catch (e: RuntimeException) {
emit(DataResult.Error.Local(e.message, e))
}
@@ -44,7 +44,7 @@ suspend inline fun FlowCollector>>.listFetcherEmitOnEmpty
} catch (e: CancellationException) {
throw e
} catch (e: ApiException) {
- emit(e.error.toDataResult())
+ emit(e.toDataResult())
} catch (e: RuntimeException) {
emit(DataResult.Error.Local(e.message, e))
}
diff --git a/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt
new file mode 100644
index 00000000..ad73d71a
--- /dev/null
+++ b/drive/base/domain/src/main/kotlin/me/proton/core/drive/base/domain/usecase/GetMemoryInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.base.domain.usecase
+
+import me.proton.core.drive.base.domain.entity.MemoryInfo
+
+interface GetMemoryInfo {
+ operator fun invoke(): Result
+}
diff --git a/drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt b/drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt
new file mode 100644
index 00000000..61cb98d9
--- /dev/null
+++ b/drive/base/domain/src/test/kotlin/me/proton/core/drive/base/domain/repository/FetcherTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.base.domain.repository
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.last
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.runTest
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.network.domain.ApiException
+import me.proton.core.network.domain.ApiResult
+import org.junit.Assert.*
+import org.junit.Test
+import java.net.UnknownHostException
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class FetcherTest {
+
+ @Test
+ fun `Given success When fetch Then emit value`() = runTest {
+ val values = flow> {
+ fetcher {
+ emit(DataResult.Success(ResponseSource.Local, Unit))
+ }
+ }.toList()
+ assertEquals(DataResult.Processing(ResponseSource.Remote), values[0])
+ assertEquals(DataResult.Success(ResponseSource.Local, Unit), values[1])
+ }
+
+ @Test
+ fun `Given api exception When fetch Then emit error with api exception`() = runTest {
+ val errorMessage = "Unable to resolve host"
+ val value = flow> {
+ fetcher {
+ throw ApiException(
+ ApiResult.Error.Connection(
+ cause = UnknownHostException(errorMessage)
+ )
+ )
+ }
+ }.last()
+
+ val error = value as DataResult.Error
+ assertEquals(errorMessage, error.message)
+ assertEquals(ResponseSource.Remote, error.source)
+ assertEquals(ApiException::class.java, error.cause?.javaClass)
+ }
+
+ @Test
+ fun `Given runtime exception When fetch Then emit error with runtime exception`() = runTest {
+ val errorMessage = "error"
+ val value = flow> {
+ fetcher {
+ throw RuntimeException(errorMessage)
+ }
+ }.last()
+
+ val error = value as DataResult.Error
+ assertEquals(errorMessage, error.message)
+ assertEquals(ResponseSource.Local, error.source)
+ assertEquals(RuntimeException::class.java, error.cause?.javaClass)
+ }
+
+ @Test
+ fun `Given cancellation exception When fetch Then emit error with runtime exception`() {
+ val flow = flow> {
+ fetcher {
+ throw CancellationException()
+ }
+ }
+ assertThrows(CancellationException::class.java) {
+ runTest { flow.last() }
+ }
+ }
+}
\ No newline at end of file
diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt
index 1c4cae51..d789012a 100644
--- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt
+++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/OutlinedTextFieldWithError.kt
@@ -22,79 +22,16 @@ import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
-import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.compose.theme.caption
import me.proton.core.compose.theme.default
-@Composable
-fun OutlinedTextFieldWithError(
- text: String,
- modifier: Modifier = Modifier,
- selection: IntRange = IntRange(text.length, text.length),
- errorText: String? = null,
- focusRequester: FocusRequester = remember { FocusRequester() },
- maxLines: Int = MaxLines,
- onValueChanged: (String) -> Unit,
-) {
- // This code is based on BasicTextField:122
- // Holds the latest internal TextFieldValue state. We need to keep it to have the correct value
- // of the composition.
- var textFieldValueState by remember {
- mutableStateOf(
- TextFieldValue(
- text = text,
- selection = TextRange(selection.first, selection.last)
- )
- )
- }
- // Holds the latest TextFieldValue that OutlinedTextFieldWithError was recomposed with.
- // We couldn't simply pass `TextFieldValue(text = text, selection=[..])` to the CoreTextField
- // because we need to preserve the composition.
- val textFieldValue = textFieldValueState.copy(
- text = text,
- selection = TextRange(selection.first, selection.last),
- )
- // Last String value that either text field was recomposed with or updated in the onValueChange
- // callback. We keep track of it to prevent calling onValueChange(String) for same String when
- // CoreTextField's onValueChange is called multiple times without recomposition in between.
- var lastTextValue by remember(text) { mutableStateOf(text) }
-
- SideEffect {
- if (textFieldValue.selection != textFieldValueState.selection ||
- textFieldValue.composition != textFieldValueState.composition
- ) {
- textFieldValueState = textFieldValue
- }
- }
- OutlinedTextFieldWithError(
- textFieldValue = textFieldValue,
- modifier = modifier,
- errorText = errorText,
- focusRequester = focusRequester,
- maxLines = maxLines,
- ) { newTextFieldValueState ->
- textFieldValueState = newTextFieldValueState
-
- val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text
- lastTextValue = newTextFieldValueState.text
-
- if (stringChangedSinceLastInvocation) {
- onValueChanged(newTextFieldValueState.text)
- }
- }
-}
-
@Composable
fun OutlinedTextFieldWithError(
textFieldValue: TextFieldValue,
diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt
index 11c6bed2..c21a433e 100644
--- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt
+++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/ProtonListItem.kt
@@ -50,7 +50,8 @@ fun ProtonListItem(
@StringRes title: Int,
modifier: Modifier = Modifier,
iconTitlePadding: Dp = ListItemTextStartPadding,
-) = ProtonListItem(painterResource(icon), stringResource(title), modifier, iconTitlePadding)
+ iconTintColor: Color = ProtonTheme.colors.iconNorm,
+) = ProtonListItem(painterResource(icon), stringResource(title), modifier, iconTitlePadding, iconTintColor)
@Composable
fun ProtonListItem(
@@ -58,6 +59,7 @@ fun ProtonListItem(
title: String,
modifier: Modifier = Modifier,
iconTitlePadding: Dp = ListItemTextStartPadding,
+ iconTintColor: Color = ProtonTheme.colors.iconNorm,
) {
Row(
modifier = modifier
@@ -72,7 +74,7 @@ fun ProtonListItem(
modifier = Modifier
.size(DefaultIconSize),
painter = icon,
- tint = ProtonTheme.colors.iconNorm,
+ tint = iconTintColor,
contentDescription = null,
)
Text(
diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/checkbox/CheckboxDefaults.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Composable.kt
similarity index 53%
rename from drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/checkbox/CheckboxDefaults.kt
rename to drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Composable.kt
index 7ca0a108..902c5296 100644
--- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/component/checkbox/CheckboxDefaults.kt
+++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Composable.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022-2023 Proton AG.
+ * Copyright (c) 2023 Proton AG.
* This file is part of Proton Core.
*
* Proton Core is free software: you can redistribute it and/or modify
@@ -15,20 +15,15 @@
* You should have received a copy of the GNU General Public License
* along with Proton Core. If not, see .
*/
-package me.proton.core.drive.base.presentation.component
-import androidx.compose.material.CheckboxColors
-import androidx.compose.material.CheckboxDefaults
+package me.proton.core.drive.base.presentation.extension
+
+import android.content.res.Configuration
import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-import me.proton.core.compose.theme.ProtonTheme
+import androidx.compose.ui.platform.LocalConfiguration
-@Composable
-fun CheckboxDefaults.protonColors(): CheckboxColors =
- colors(
- checkedColor = ProtonTheme.colors.interactionNorm,
- uncheckedColor = ProtonTheme.colors.shade60,
- checkmarkColor = Color.White,
- disabledColor = ProtonTheme.colors.shade40,
- disabledIndeterminateColor = ProtonTheme.colors.shade40,
- )
+val isPortrait: Boolean @Composable get() =
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT
+
+val isLandscape: Boolean @Composable get() =
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
diff --git a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt
index 7dd8e081..519b2856 100644
--- a/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt
+++ b/drive/base/presentation/src/main/kotlin/me/proton/core/drive/base/presentation/extension/Modifier.kt
@@ -18,6 +18,13 @@
package me.proton.core.drive.base.presentation.extension
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
fun Modifier.conditional(condition: Boolean, block: Modifier.() -> Modifier) =
if (condition) {
@@ -25,3 +32,36 @@ fun Modifier.conditional(condition: Boolean, block: Modifier.() -> Modifier) =
} else {
this
}
+
+fun Modifier.shadow(
+ color: Color = Color.Black,
+ alpha: Float = 1f,
+ cornersRadius: Dp = 0.dp,
+ blurRadius: Dp = 0.dp,
+ offsetX: Dp = 0.dp,
+ offsetY: Dp = 0.dp,
+) = drawBehind {
+ val shadow = color.copy(alpha = alpha).toArgb()
+ val transparent = color.copy(alpha = 0f).toArgb()
+
+ drawIntoCanvas { canvas ->
+ val paint = Paint()
+ val frameworkPaint = paint.asFrameworkPaint()
+ frameworkPaint.color = transparent
+ frameworkPaint.setShadowLayer(
+ blurRadius.toPx(),
+ offsetX.toPx(),
+ offsetY.toPx(),
+ shadow,
+ )
+ canvas.drawRoundRect(
+ left = 0f,
+ top = 0f,
+ right = size.width,
+ bottom = size.height,
+ radiusX = cornersRadius.toPx(),
+ radiusY = cornersRadius.toPx(),
+ paint = paint,
+ )
+ }
+}
diff --git a/drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml b/drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml
new file mode 100644
index 00000000..c10a83f0
--- /dev/null
+++ b/drive/base/presentation/src/main/res/drawable/ic_checkmark_circle_filled.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml b/drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml
new file mode 100644
index 00000000..a63cff7f
--- /dev/null
+++ b/drive/base/presentation/src/main/res/drawable/ic_proton_lock_open.xml
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/drive/base/presentation/src/main/res/values/strings.xml b/drive/base/presentation/src/main/res/values/strings.xml
index 9c95a93f..c972c990 100644
--- a/drive/base/presentation/src/main/res/values/strings.xml
+++ b/drive/base/presentation/src/main/res/values/strings.xml
@@ -424,4 +424,24 @@
- %1$d item
- %1$d items
+
+
+ App locked
+ Unlock using your device lock
+ Confirmation
+ To enable unlock, confirm with your device lock
+ To disable unlock, confirm with your device lock
+ Enable biometrics or device lock
+ Keep unlocked
+ Biometrics or device lock
+ Turn on biometrics access
+ Set up biometrics or device lock access on this device.
+ Settings
+ Biometrics or device lock is not available on your device
+ Biometrics authentication failed
+ Biometrics or device lock access activated
+ App lock has been deactivated
+ Choose how to lock the app
+ Unlock the app
+
diff --git a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt
index a2af1c7f..62a9b3e9 100644
--- a/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt
+++ b/drive/block/data/src/main/kotlin/me/proton/core/drive/block/data/repository/BlockRepositoryImpl.kt
@@ -30,7 +30,7 @@ import javax.inject.Inject
class BlockRepositoryImpl @Inject constructor(
private val api: BlockApiDataSource,
-): BlockRepository {
+) : BlockRepository {
override suspend fun getUploadBlocksUrl(
userId: UserId,
addressId: AddressId,
@@ -39,13 +39,17 @@ class BlockRepositoryImpl @Inject constructor(
uploadBlocks: List,
uploadThumbnail: UploadBlock?,
): Result = coRunCatching {
- api.uploadBlock(
- userId = userId,
- addressId = addressId,
- fileId = fileId,
- revisionId = revisionId,
- uploadBlocks = uploadBlocks,
- uploadThumbnail = uploadThumbnail,
- ).toUploadBlocksUrl()
+ if (uploadThumbnail != null || uploadBlocks.isNotEmpty()) {
+ api.uploadBlock(
+ userId = userId,
+ addressId = addressId,
+ fileId = fileId,
+ revisionId = revisionId,
+ uploadBlocks = uploadBlocks,
+ uploadThumbnail = uploadThumbnail,
+ ).toUploadBlocksUrl()
+ } else {
+ UploadBlocksUrl(emptyList(), null)
+ }
}
}
diff --git a/drive/crypto-base/domain/build.gradle.kts b/drive/crypto-base/domain/build.gradle.kts
index 38028a20..6be398db 100644
--- a/drive/crypto-base/domain/build.gradle.kts
+++ b/drive/crypto-base/domain/build.gradle.kts
@@ -21,7 +21,7 @@ plugins {
driveModule(hilt = true) {
api(project(":drive:base:domain"))
- api(libs.core.auth)
- api(libs.core.crypto)
- api(libs.core.key)
+ api(libs.core.auth.domain)
+ api(libs.core.cryptoCommon)
+ api(libs.core.key.domain)
}
diff --git a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt
index d6de0429..d426746b 100644
--- a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt
+++ b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/extension/UserAddressRepository.kt
@@ -24,16 +24,16 @@ import me.proton.core.user.domain.entity.AddressId
import me.proton.core.user.domain.repository.UserAddressRepository
suspend fun UserAddressRepository.getAddressKeys(userId: UserId, email: String): KeyHolder =
- getAddresses(userId)
- .first { userAddress -> userAddress.email == email }
- .keys
- .keyHolder()
+ with (getAddresses(userId)) {
+ firstOrNull { userAddress -> userAddress.email == email }?.keys?.keyHolder()
+ ?: flatMap { userAddress -> userAddress.keys }.keyHolder()
+ }
suspend fun UserAddressRepository.getAddressKeys(userId: UserId, addressId: AddressId): KeyHolder =
- getAddresses(userId)
- .first { userAddress -> userAddress.addressId == addressId }
- .keys
- .keyHolder()
+ with (getAddresses(userId)) {
+ firstOrNull { userAddress -> userAddress.addressId == addressId }?.keys?.keyHolder()
+ ?: flatMap { userAddress -> userAddress.keys }.keyHolder()
+ }
fun List.keyHolder() = object : KeyHolder {
override val keys: List = this@keyHolder
diff --git a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt
index a0686e76..bcc32799 100644
--- a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt
+++ b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/DecryptNestedPrivateKey.kt
@@ -17,6 +17,7 @@
*/
package me.proton.core.drive.cryptobase.domain.usecase
+import kotlinx.coroutines.withContext
import me.proton.core.crypto.common.context.CryptoContext
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.domain.util.coRunCatching
@@ -70,12 +71,14 @@ class DecryptNestedPrivateKey @Inject constructor(
signatureAddress: String,
allowCompromisedVerificationKeys: Boolean = false,
coroutineContext: CoroutineContext = CryptoScope.EncryptAndDecrypt.coroutineContext,
- ): Result =
+ ): Result = withContext(coroutineContext) {
invoke(
decryptKey = decryptKey,
key = key,
- verifyKeyRing = userAddressRepository.getAddressKeys(userId, signatureAddress).publicKeyRing(cryptoContext),
+ verifyKeyRing = userAddressRepository.getAddressKeys(userId, signatureAddress)
+ .publicKeyRing(cryptoContext),
allowCompromisedVerificationKeys = allowCompromisedVerificationKeys,
coroutineContext = coroutineContext,
)
+ }
}
diff --git a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt
index 6aedea52..068904f1 100644
--- a/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt
+++ b/drive/crypto-base/domain/src/main/kotlin/me/proton/core/drive/cryptobase/domain/usecase/GenerateSrpForShareUrl.kt
@@ -19,16 +19,20 @@ package me.proton.core.drive.cryptobase.domain.usecase
import me.proton.core.auth.domain.repository.AuthRepository
import me.proton.core.crypto.common.context.CryptoContext
+import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.domain.util.coRunCatching
import me.proton.core.drive.cryptobase.domain.entity.SrpForShareUrl
+import me.proton.core.network.domain.session.SessionProvider
import javax.inject.Inject
class GenerateSrpForShareUrl @Inject constructor(
private val cryptoContext: CryptoContext,
private val authRepository: AuthRepository,
+ private val sessionProvider: SessionProvider,
) {
- suspend operator fun invoke(urlPassword: ByteArray) = coRunCatching {
- val modulus = authRepository.randomModulus()
+ suspend operator fun invoke(userId: UserId, urlPassword: ByteArray) = coRunCatching {
+ val sessionId = sessionProvider.getSessionId(userId)
+ val modulus = authRepository.randomModulus(sessionId)
val auth = cryptoContext.srpCrypto.calculatePasswordVerifier(
username = "",
password = urlPassword,
diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt
index 97eb9920..9059203c 100644
--- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt
+++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlCustomPasswordInfo.kt
@@ -53,7 +53,7 @@ class CreateShareUrlCustomPasswordInfo @Inject constructor(
.getOrThrow()
.take(RANDOM_URL_PASSWORD_SIZE)
val urlPassword = "$randomPassword$customPassword"
- val srpForShareUrl = generateSrpForShareUrl(urlPassword.toByteArray()).getOrThrow()
+ val srpForShareUrl = generateSrpForShareUrl(userId, urlPassword.toByteArray()).getOrThrow()
val addressKeys = getAddressKeys(userId, addressId)
val reencryptedSharePassphrase = reencryptSharePassphraseWithUrlPassword(
decryptKey = addressKeys.keyHolder,
diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt
index d842d9bc..8d289183 100644
--- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt
+++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/share/CreateShareUrlInfo.kt
@@ -46,7 +46,7 @@ class CreateShareUrlInfo @Inject constructor(
"Random URL password size (${randomUrlPassword.length}) does not match requirement (${RANDOM_URL_PASSWORD_SIZE})"
}
val addressKeys = getAddressKeys(userId, addressId)
- val srpForShareUrl = generateSrpForShareUrl(randomUrlPassword.toByteArray()).getOrThrow()
+ val srpForShareUrl = generateSrpForShareUrl(userId, randomUrlPassword.toByteArray()).getOrThrow()
val reencryptedSharePassphrase = reencryptSharePassphraseWithUrlPassword(
decryptKey = addressKeys.keyHolder,
urlPassword = randomUrlPassword.toByteArray(),
diff --git a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt
index 47afed0c..28f47152 100644
--- a/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt
+++ b/drive/crypto/domain/src/main/kotlin/me/proton/core/drive/crypto/domain/usecase/upload/EncryptUploadBlocks.kt
@@ -54,36 +54,22 @@ class EncryptUploadBlocks @Inject constructor(
coroutineContext = coroutineContext,
) { sessionKey ->
input.mapIndexed { index, input ->
- if (input.length() == 0L) {
- block(
- index,
- input,
- output[index].apply { createNewFile() },
- encryptedSignature(
- unlockedEncryptKey = unlockedSignatureEncryptionKey,
- unlockedSignKey = unlockedFileSignKey,
- input = ByteArray(size = 0),
- coroutineContext = coroutineContext,
- ).getOrThrow()
- )
- } else {
- block(
- index,
- input,
- encryptFile(
- encryptKey = sessionKey,
- source = input,
- destination = output[index],
- coroutineContext = coroutineContext,
- ).getOrThrow(),
- encryptedSignature(
- unlockedEncryptKey = unlockedSignatureEncryptionKey,
- unlockedSignKey = unlockedFileSignKey,
- file = input,
- coroutineContext = coroutineContext,
- ).getOrThrow()
- )
- }
+ block(
+ index,
+ input,
+ encryptFile(
+ encryptKey = sessionKey,
+ source = input,
+ destination = output[index],
+ coroutineContext = coroutineContext,
+ ).getOrThrow(),
+ encryptedSignature(
+ unlockedEncryptKey = unlockedSignatureEncryptionKey,
+ unlockedSignKey = unlockedFileSignKey,
+ file = input,
+ coroutineContext = coroutineContext,
+ ).getOrThrow()
+ )
}
}.getOrThrow()
}.getOrThrow()
diff --git a/drive/db/build.gradle.kts b/drive/db/build.gradle.kts
new file mode 100644
index 00000000..d820e529
--- /dev/null
+++ b/drive/db/build.gradle.kts
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2021-2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+plugins {
+ id("com.android.library")
+}
+
+driveModule(
+ hilt = true,
+ room = true,
+) {
+ api(libs.core.account.data)
+ api(libs.core.challenge.data)
+ api(libs.core.crypto.android)
+ api(libs.core.eventManager.data)
+ api(libs.core.featureFlag.data)
+ api(libs.core.humanVerification.data)
+ api(libs.core.key.data)
+ api(libs.core.observability.data)
+ api(libs.core.payment.data)
+ api(libs.core.user.data)
+ api(libs.core.userSettings.data)
+ // TODO: Extract from drive db
+ api(project(":app-ui-settings"))
+ api(project(":drive:drivelink:data"))
+ api(project(":drive:drivelink-download:data"))
+ api(project(":drive:drivelink-offline:data"))
+ api(project(":drive:drivelink-paged:data"))
+ api(project(":drive:drivelink-selection:data"))
+ api(project(":drive:drivelink-shared:data"))
+ api(project(":drive:drivelink-trash:data"))
+ api(project(":drive:folder:data"))
+ api(project(":drive:link:data"))
+ api(project(":drive:link-download:data"))
+ api(project(":drive:link-node:data"))
+ api(project(":drive:link-offline:data"))
+ api(project(":drive:link-selection:data"))
+ api(project(":drive:link-trash:data"))
+ api(project(":drive:link-upload:data"))
+ api(project(":drive:message-queue:data"))
+ api(project(":drive:notification:data"))
+ api(project(":drive:share:data"))
+ api(project(":drive:share-url:base:data"))
+ api(project(":drive:sorting:data"))
+ api(project(":drive:volume:data"))
+}
diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json
new file mode 100644
index 00000000..8d06834d
--- /dev/null
+++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/1.json
@@ -0,0 +1,3233 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 1,
+ "identityHash": "1848c5aa24ef749bea72f819f081cfe5",
+ "entities": [
+ {
+ "tableName": "AccountEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionState",
+ "columnName": "sessionState",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_AccountEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AccountMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primaryAtUtc",
+ "columnName": "primaryAtUtc",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrations",
+ "columnName": "migrations",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "product"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_product",
+ "unique": false,
+ "columnNames": [
+ "product"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_primaryAtUtc",
+ "unique": false,
+ "columnNames": [
+ "primaryAtUtc"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accessToken",
+ "columnName": "accessToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "refreshToken",
+ "columnName": "refreshToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scopes",
+ "columnName": "scopes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_SessionEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionDetailsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "initialEventId",
+ "columnName": "initialEventId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "requiredAccountType",
+ "columnName": "requiredAccountType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secondFactorEnabled",
+ "columnName": "secondFactorEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "twoPassModeEnabled",
+ "columnName": "twoPassModeEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionDetailsEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "credit",
+ "columnName": "credit",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxUpload",
+ "columnName": "maxUpload",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "role",
+ "columnName": "role",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "private",
+ "columnName": "private",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscribed",
+ "columnName": "subscribed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "services",
+ "columnName": "services",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "delinquent",
+ "columnName": "delinquent",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserKeyEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_UserKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "domainId",
+ "columnName": "domainId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "canSend",
+ "columnName": "canSend",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canReceive",
+ "columnName": "canReceive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signedKeyList.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "addressId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprints",
+ "columnName": "fingerprints",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressKeyEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "addressId"
+ ],
+ "referencedColumns": [
+ "addressId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "KeySaltEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keySalt",
+ "columnName": "keySalt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_KeySaltEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_KeySaltEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientType",
+ "columnName": "recipientType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email",
+ "publicKey"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressKeyEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "PublicAddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "email"
+ ],
+ "referencedColumns": [
+ "email"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "HumanVerificationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `captchaVerificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))",
+ "fields": [
+ {
+ "fieldPath": "clientId",
+ "columnName": "clientId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clientIdType",
+ "columnName": "clientIdType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationMethods",
+ "columnName": "verificationMethods",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "captchaVerificationToken",
+ "columnName": "captchaVerificationToken",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "humanHeaderTokenType",
+ "columnName": "humanHeaderTokenType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "humanHeaderTokenCode",
+ "columnName": "humanHeaderTokenCode",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "clientId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "UserSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `twoFA_u2fKeys` TEXT, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "news",
+ "columnName": "news",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "logAuth",
+ "columnName": "logAuth",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "invoiceText",
+ "columnName": "invoiceText",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "density",
+ "columnName": "density",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "themeType",
+ "columnName": "themeType",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "weekStart",
+ "columnName": "weekStart",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateFormat",
+ "columnName": "dateFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timeFormat",
+ "columnName": "timeFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "welcome",
+ "columnName": "welcome",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "earlyAccess",
+ "columnName": "earlyAccess",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.value",
+ "columnName": "email_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.status",
+ "columnName": "email_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.notify",
+ "columnName": "email_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.reset",
+ "columnName": "email_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.value",
+ "columnName": "phone_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.status",
+ "columnName": "phone_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.notify",
+ "columnName": "phone_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.reset",
+ "columnName": "phone_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.mode",
+ "columnName": "password_mode",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.expirationTime",
+ "columnName": "password_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.enabled",
+ "columnName": "twoFA_enabled",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.allowed",
+ "columnName": "twoFA_allowed",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.expirationTime",
+ "columnName": "twoFA_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.u2fKeys",
+ "columnName": "twoFA_u2fKeys",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags.welcomed",
+ "columnName": "flags_welcomed",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "planName",
+ "columnName": "planName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "vpnPlanName",
+ "columnName": "vpnPlanName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFactorGracePeriod",
+ "columnName": "twoFactorGracePeriod",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxDomains",
+ "columnName": "maxDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAddresses",
+ "columnName": "maxAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxMembers",
+ "columnName": "maxMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxVPN",
+ "columnName": "maxVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "features",
+ "columnName": "features",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedDomains",
+ "columnName": "usedDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedAddresses",
+ "columnName": "usedAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "assignedSpace",
+ "columnName": "assignedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedMembers",
+ "columnName": "usedMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedVPN",
+ "columnName": "usedVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hasKeys",
+ "columnName": "hasKeys",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "toMigrate",
+ "columnName": "toMigrate",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationKeysEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "EventMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "config",
+ "columnName": "config",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextEventId",
+ "columnName": "nextEventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "refresh",
+ "columnName": "refresh",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "more",
+ "columnName": "more",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "response",
+ "columnName": "response",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "retry",
+ "columnName": "retry",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "config"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_EventMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_config",
+ "unique": false,
+ "columnNames": [
+ "config"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_createdAt",
+ "unique": false,
+ "columnNames": [
+ "createdAt"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "VolumeEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "max_space",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "used_space",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_VolumeEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `block_size` INTEGER NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "blockSize",
+ "columnName": "block_size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLocked",
+ "columnName": "locked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphraseSignature",
+ "columnName": "passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_ShareEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareUrlEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creatorEmail",
+ "columnName": "creatior_email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastAccessTime",
+ "columnName": "last_access_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAccesses",
+ "columnName": "max_accesses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "urlPasswordSalt",
+ "columnName": "url_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePasswordSalt",
+ "columnName": "share_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpVerifier",
+ "columnName": "srp_verifier",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpModulusId",
+ "columnName": "srp_modulus_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePassphraseKeyPacket",
+ "columnName": "share_passphrase_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareUrlEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameSignatureEmail",
+ "columnName": "name_signature_email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attributes",
+ "columnName": "attributes",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trashedTime",
+ "columnName": "trashed_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shared",
+ "columnName": "is_shared",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareUrlExpirationTime",
+ "columnName": "share_url_expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id_parent_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFilePropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "file_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "file_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "file_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "activeRevisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasThumbnail",
+ "columnName": "has_thumbnail",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activeRevisionSignatureAddress",
+ "columnName": "file_signature_address",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFolderPropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "folder_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "folder_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "folder_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeHashKey",
+ "columnName": "node_hash_key",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkOfflineEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkOfflineEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkDownloadStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DownloadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkDownloadStateEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkTrashStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkTrashStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashWorkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "workId",
+ "columnName": "work_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_TrashWorkEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_TrashWorkEntity_work_id",
+ "unique": false,
+ "columnNames": [
+ "work_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "MessageEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_MessageEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UiSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "layoutType",
+ "columnName": "layout_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "themeStyle",
+ "columnName": "theme_style",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DriveLinkRemoteKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "prevKey",
+ "columnName": "previous_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextKey",
+ "columnName": "next_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "key",
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SortingEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingBy",
+ "columnName": "sorting_by",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingDirection",
+ "columnName": "sorting_direction",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkUploadEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_LinkUploadEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uploadLinkId",
+ "columnName": "upload_link_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uploadToken",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_link_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkUploadEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_link_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FolderMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchChildrenTimestamp",
+ "columnName": "last_fetch_children_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchTrashTimestamp",
+ "columnName": "last_fetch_trash_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1848c5aa24ef749bea72f819f081cfe5')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/10.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/10.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/10.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/10.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/11.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/11.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/11.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/11.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/12.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/12.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/12.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/12.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/13.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/13.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/13.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/13.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/14.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/14.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/14.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/14.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/15.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/15.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/15.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/15.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/16.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/16.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/16.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/16.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/17.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/17.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/17.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/17.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/18.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/18.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/18.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/18.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/19.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/19.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/19.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/19.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/2.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/2.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/2.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/2.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/20.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/20.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/20.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/20.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/21.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/21.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/21.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/21.json
diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json
new file mode 100644
index 00000000..4520cac4
--- /dev/null
+++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/22.json
@@ -0,0 +1,4237 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 22,
+ "identityHash": "294985598d7bdcd4fa1e0709f83e5ead",
+ "entities": [
+ {
+ "tableName": "AccountEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionState",
+ "columnName": "sessionState",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_AccountEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AccountMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primaryAtUtc",
+ "columnName": "primaryAtUtc",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrations",
+ "columnName": "migrations",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "product"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_product",
+ "unique": false,
+ "columnNames": [
+ "product"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_primaryAtUtc",
+ "unique": false,
+ "columnNames": [
+ "primaryAtUtc"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accessToken",
+ "columnName": "accessToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "refreshToken",
+ "columnName": "refreshToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scopes",
+ "columnName": "scopes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_SessionEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionDetailsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "initialEventId",
+ "columnName": "initialEventId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "requiredAccountType",
+ "columnName": "requiredAccountType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secondFactorEnabled",
+ "columnName": "secondFactorEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "twoPassModeEnabled",
+ "columnName": "twoPassModeEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionDetailsEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "credit",
+ "columnName": "credit",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxUpload",
+ "columnName": "maxUpload",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "role",
+ "columnName": "role",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isPrivate",
+ "columnName": "private",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscribed",
+ "columnName": "subscribed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "services",
+ "columnName": "services",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "delinquent",
+ "columnName": "delinquent",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserKeyEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_UserKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "domainId",
+ "columnName": "domainId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "canSend",
+ "columnName": "canSend",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canReceive",
+ "columnName": "canReceive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signedKeyList.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "addressId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprints",
+ "columnName": "fingerprints",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressKeyEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "addressId"
+ ],
+ "referencedColumns": [
+ "addressId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "KeySaltEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keySalt",
+ "columnName": "keySalt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_KeySaltEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_KeySaltEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientType",
+ "columnName": "recipientType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email",
+ "publicKey"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressKeyEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "PublicAddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "email"
+ ],
+ "referencedColumns": [
+ "email"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "HumanVerificationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))",
+ "fields": [
+ {
+ "fieldPath": "clientId",
+ "columnName": "clientId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clientIdType",
+ "columnName": "clientIdType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationMethods",
+ "columnName": "verificationMethods",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationToken",
+ "columnName": "verificationToken",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "humanHeaderTokenType",
+ "columnName": "humanHeaderTokenType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "humanHeaderTokenCode",
+ "columnName": "humanHeaderTokenCode",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "clientId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "UserSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "news",
+ "columnName": "news",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "logAuth",
+ "columnName": "logAuth",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "invoiceText",
+ "columnName": "invoiceText",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "density",
+ "columnName": "density",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "themeType",
+ "columnName": "themeType",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "weekStart",
+ "columnName": "weekStart",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateFormat",
+ "columnName": "dateFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timeFormat",
+ "columnName": "timeFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "welcome",
+ "columnName": "welcome",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "earlyAccess",
+ "columnName": "earlyAccess",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.value",
+ "columnName": "email_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.status",
+ "columnName": "email_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.notify",
+ "columnName": "email_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.reset",
+ "columnName": "email_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.value",
+ "columnName": "phone_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.status",
+ "columnName": "phone_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.notify",
+ "columnName": "phone_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.reset",
+ "columnName": "phone_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.mode",
+ "columnName": "password_mode",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.expirationTime",
+ "columnName": "password_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.enabled",
+ "columnName": "twoFA_enabled",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.allowed",
+ "columnName": "twoFA_allowed",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.expirationTime",
+ "columnName": "twoFA_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags.welcomed",
+ "columnName": "flags_welcomed",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "planName",
+ "columnName": "planName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "vpnPlanName",
+ "columnName": "vpnPlanName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFactorGracePeriod",
+ "columnName": "twoFactorGracePeriod",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxDomains",
+ "columnName": "maxDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAddresses",
+ "columnName": "maxAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxMembers",
+ "columnName": "maxMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxVPN",
+ "columnName": "maxVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxCalendars",
+ "columnName": "maxCalendars",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "features",
+ "columnName": "features",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedDomains",
+ "columnName": "usedDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedAddresses",
+ "columnName": "usedAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "assignedSpace",
+ "columnName": "assignedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedMembers",
+ "columnName": "usedMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedVPN",
+ "columnName": "usedVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedCalendars",
+ "columnName": "usedCalendars",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hasKeys",
+ "columnName": "hasKeys",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "toMigrate",
+ "columnName": "toMigrate",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationKeysEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "EventMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "config",
+ "columnName": "config",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextEventId",
+ "columnName": "nextEventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "refresh",
+ "columnName": "refresh",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "more",
+ "columnName": "more",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "response",
+ "columnName": "response",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "retry",
+ "columnName": "retry",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "config"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_EventMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_config",
+ "unique": false,
+ "columnNames": [
+ "config"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_createdAt",
+ "unique": false,
+ "columnNames": [
+ "createdAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FeatureFlagEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "featureId",
+ "columnName": "featureId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultValue",
+ "columnName": "defaultValue",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "featureId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_FeatureFlagEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_FeatureFlagEntity_featureId",
+ "unique": false,
+ "columnNames": [
+ "featureId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ChallengeFrameEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))",
+ "fields": [
+ {
+ "fieldPath": "challengeFrame",
+ "columnName": "challengeFrame",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flow",
+ "columnName": "flow",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTime",
+ "columnName": "focusTime",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clicks",
+ "columnName": "clicks",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "copy",
+ "columnName": "copy",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paste",
+ "columnName": "paste",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keys",
+ "columnName": "keys",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "challengeFrame"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GooglePurchaseEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))",
+ "fields": [
+ {
+ "fieldPath": "googlePurchaseToken",
+ "columnName": "googlePurchaseToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paymentToken",
+ "columnName": "paymentToken",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "googlePurchaseToken"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_GooglePurchaseEntity_paymentToken",
+ "unique": true,
+ "columnNames": [
+ "paymentToken"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ObservabilityEventEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "data",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "VolumeEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "max_space",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "used_space",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_VolumeEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `address_id` TEXT, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, `creation_time` INTEGER, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "address_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLocked",
+ "columnName": "locked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphraseSignature",
+ "columnName": "passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_ShareEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_ShareEntity_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ShareEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareUrlEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, `public_url` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creatorEmail",
+ "columnName": "creatior_email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastAccessTime",
+ "columnName": "last_access_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAccesses",
+ "columnName": "max_accesses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "urlPasswordSalt",
+ "columnName": "url_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePasswordSalt",
+ "columnName": "share_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpVerifier",
+ "columnName": "srp_verifier",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpModulusId",
+ "columnName": "srp_modulus_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedUrlPassword",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePassphraseKeyPacket",
+ "columnName": "share_passphrase_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicUrl",
+ "columnName": "public_url",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareUrlEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareUrlEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_ShareUrlEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, `x_attr` TEXT, `share_url_share_id` TEXT DEFAULT NULL, `share_url_id` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameSignatureEmail",
+ "columnName": "name_signature_email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attributes",
+ "columnName": "attributes",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trashedTime",
+ "columnName": "trashed_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shared",
+ "columnName": "is_shared",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareUrlExpirationTime",
+ "columnName": "share_url_expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "xAttr",
+ "columnName": "x_attr",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shareUrlShareId",
+ "columnName": "share_url_share_id",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "shareUrlId",
+ "columnName": "share_url_id",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ },
+ {
+ "name": "index_LinkEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_id` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id_parent_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFilePropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "file_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "file_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "file_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "activeRevisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasThumbnail",
+ "columnName": "has_thumbnail",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activeRevisionSignatureAddress",
+ "columnName": "file_signature_address",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFilePropertiesEntity_file_share_id",
+ "unique": false,
+ "columnNames": [
+ "file_share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_share_id` ON `${TABLE_NAME}` (`file_share_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_link_id` ON `${TABLE_NAME}` (`file_link_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFolderPropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "folder_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "folder_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "folder_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeHashKey",
+ "columnName": "node_hash_key",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_share_id",
+ "unique": false,
+ "columnNames": [
+ "folder_share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_share_id` ON `${TABLE_NAME}` (`folder_share_id`)"
+ },
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_link_id` ON `${TABLE_NAME}` (`folder_link_id`)"
+ },
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkOfflineEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkOfflineEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkDownloadStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, `manifest_signature` TEXT DEFAULT NULL, `signature_address` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkDownloadStateEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_state",
+ "unique": false,
+ "columnNames": [
+ "state"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_state` ON `${TABLE_NAME}` (`state`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DownloadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DownloadBlockEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkDownloadStateEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkTrashStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkTrashStateEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_state",
+ "unique": false,
+ "columnNames": [
+ "state"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_state` ON `${TABLE_NAME}` (`state`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashWorkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "workId",
+ "columnName": "work_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_TrashWorkEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_TrashWorkEntity_work_id",
+ "unique": false,
+ "columnNames": [
+ "work_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "MessageEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_MessageEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UiSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "layoutType",
+ "columnName": "layout_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "themeStyle",
+ "columnName": "theme_style",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DriveLinkRemoteKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "prevKey",
+ "columnName": "previous_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextKey",
+ "columnName": "next_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "key",
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_key",
+ "unique": false,
+ "columnNames": [
+ "key"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_key` ON `${TABLE_NAME}` (`key`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SortingEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingBy",
+ "columnName": "sorting_by",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingDirection",
+ "columnName": "sorting_direction",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkUploadEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, `size` INTEGER DEFAULT NULL, `last_modified` INTEGER, `uri` TEXT DEFAULT NULL, `should_delete_source_uri` INTEGER NOT NULL DEFAULT false, `media_resolution_width` INTEGER DEFAULT NULL, `media_resolution_height` INTEGER DEFAULT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "shouldDeleteSourceUri",
+ "columnName": "should_delete_source_uri",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "mediaResolutionWidth",
+ "columnName": "media_resolution_width",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "mediaResolutionHeight",
+ "columnName": "media_resolution_height",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_LinkUploadEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, `raw_size` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uploadLinkId",
+ "columnName": "upload_link_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uploadToken",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawSize",
+ "columnName": "raw_size",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_link_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkUploadEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_link_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBulkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `should_delete_source_uri` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shouldDeleteSourceUri",
+ "columnName": "should_delete_source_uri",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_UploadBulkEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBulkUriStringEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_bulk_id` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`upload_bulk_id`, `uri`), FOREIGN KEY(`upload_bulk_id`) REFERENCES `UploadBulkEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "upload_bulk_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_bulk_id",
+ "uri"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UploadBulkUriStringEntity_upload_bulk_id",
+ "unique": false,
+ "columnNames": [
+ "upload_bulk_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkUriStringEntity_upload_bulk_id` ON `${TABLE_NAME}` (`upload_bulk_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UploadBulkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_bulk_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FolderMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchChildrenTimestamp",
+ "columnName": "last_fetch_children_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchTrashTimestamp",
+ "columnName": "last_fetch_trash_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NotificationChannelEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`user_id`, `type`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "type"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_NotificationChannelEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationChannelEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NotificationEventEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `channel_type` TEXT NOT NULL, `notification_tag` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, `notification_event_id` TEXT NOT NULL, `notification_event` TEXT NOT NULL, PRIMARY KEY(`user_id`, `channel_type`, `notification_tag`, `notification_id`, `notification_event_id`), FOREIGN KEY(`user_id`, `channel_type`) REFERENCES `NotificationChannelEntity`(`user_id`, `type`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "channelType",
+ "columnName": "channel_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationTag",
+ "columnName": "notification_tag",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationId",
+ "columnName": "notification_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationEventId",
+ "columnName": "notification_event_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationEvent",
+ "columnName": "notification_event",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "channel_type",
+ "notification_tag",
+ "notification_id",
+ "notification_event_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "channel_type",
+ "notification_tag",
+ "notification_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id` ON `${TABLE_NAME}` (`user_id`, `channel_type`, `notification_tag`, `notification_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "NotificationChannelEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "channel_type"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "type"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkSelectionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `selection_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `selection_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "selectionId",
+ "columnName": "selection_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "selection_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkSelectionEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_selection_id",
+ "unique": false,
+ "columnNames": [
+ "selection_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_selection_id` ON `${TABLE_NAME}` (`selection_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '294985598d7bdcd4fa1e0709f83e5ead')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json
new file mode 100644
index 00000000..efaae2b9
--- /dev/null
+++ b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/23.json
@@ -0,0 +1,4244 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 23,
+ "identityHash": "59591b661e49ae97591acd9c77f82223",
+ "entities": [
+ {
+ "tableName": "AccountEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `username` TEXT NOT NULL, `email` TEXT, `state` TEXT NOT NULL, `sessionId` TEXT, `sessionState` TEXT, PRIMARY KEY(`userId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "username",
+ "columnName": "username",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionState",
+ "columnName": "sessionState",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_AccountEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "NO ACTION",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AccountMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `product` TEXT NOT NULL, `primaryAtUtc` INTEGER NOT NULL, `migrations` TEXT, PRIMARY KEY(`userId`, `product`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "primaryAtUtc",
+ "columnName": "primaryAtUtc",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "migrations",
+ "columnName": "migrations",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "product"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AccountMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_product",
+ "unique": false,
+ "columnNames": [
+ "product"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_product` ON `${TABLE_NAME}` (`product`)"
+ },
+ {
+ "name": "index_AccountMetadataEntity_primaryAtUtc",
+ "unique": false,
+ "columnNames": [
+ "primaryAtUtc"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AccountMetadataEntity_primaryAtUtc` ON `${TABLE_NAME}` (`primaryAtUtc`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT, `sessionId` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `refreshToken` TEXT NOT NULL, `scopes` TEXT NOT NULL, `product` TEXT NOT NULL, PRIMARY KEY(`sessionId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "accessToken",
+ "columnName": "accessToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "refreshToken",
+ "columnName": "refreshToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scopes",
+ "columnName": "scopes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "product",
+ "columnName": "product",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ },
+ {
+ "name": "index_SessionEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SessionDetailsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sessionId` TEXT NOT NULL, `initialEventId` TEXT NOT NULL, `requiredAccountType` TEXT NOT NULL, `secondFactorEnabled` INTEGER NOT NULL, `twoPassModeEnabled` INTEGER NOT NULL, `password` TEXT, PRIMARY KEY(`sessionId`), FOREIGN KEY(`sessionId`) REFERENCES `SessionEntity`(`sessionId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "sessionId",
+ "columnName": "sessionId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "initialEventId",
+ "columnName": "initialEventId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "requiredAccountType",
+ "columnName": "requiredAccountType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secondFactorEnabled",
+ "columnName": "secondFactorEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "twoPassModeEnabled",
+ "columnName": "twoPassModeEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "password",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "sessionId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_SessionDetailsEntity_sessionId",
+ "unique": false,
+ "columnNames": [
+ "sessionId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_SessionDetailsEntity_sessionId` ON `${TABLE_NAME}` (`sessionId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "SessionEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "sessionId"
+ ],
+ "referencedColumns": [
+ "sessionId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `email` TEXT, `name` TEXT, `displayName` TEXT, `currency` TEXT NOT NULL, `credit` INTEGER NOT NULL, `usedSpace` INTEGER NOT NULL, `maxSpace` INTEGER NOT NULL, `maxUpload` INTEGER NOT NULL, `role` INTEGER, `private` INTEGER NOT NULL, `subscribed` INTEGER NOT NULL, `services` INTEGER NOT NULL, `delinquent` INTEGER, `passphrase` BLOB, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "currency",
+ "columnName": "currency",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "credit",
+ "columnName": "credit",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxUpload",
+ "columnName": "maxUpload",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "role",
+ "columnName": "role",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "isPrivate",
+ "columnName": "private",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "subscribed",
+ "columnName": "subscribed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "services",
+ "columnName": "services",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "delinquent",
+ "columnName": "delinquent",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UserKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `fingerprint` TEXT, `activation` TEXT, `active` INTEGER, PRIMARY KEY(`keyId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UserKeyEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_UserKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UserKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `addressId` TEXT NOT NULL, `email` TEXT NOT NULL, `displayName` TEXT, `signature` TEXT, `domainId` TEXT, `canSend` INTEGER NOT NULL, `canReceive` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `type` INTEGER, `order` INTEGER NOT NULL, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`addressId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "domainId",
+ "columnName": "domainId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "canSend",
+ "columnName": "canSend",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "canReceive",
+ "columnName": "canReceive",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signedKeyList.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyList.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "addressId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "AddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`addressId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `version` INTEGER NOT NULL, `privateKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, `isUnlockable` INTEGER NOT NULL, `flags` INTEGER NOT NULL, `passphrase` BLOB, `token` TEXT, `signature` TEXT, `fingerprint` TEXT, `fingerprints` TEXT, `activation` TEXT, `active` INTEGER NOT NULL, PRIMARY KEY(`keyId`), FOREIGN KEY(`addressId`) REFERENCES `AddressEntity`(`addressId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "addressId",
+ "columnName": "addressId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isUnlockable",
+ "columnName": "isUnlockable",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "BLOB",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signature",
+ "columnName": "signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprint",
+ "columnName": "fingerprint",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "fingerprints",
+ "columnName": "fingerprints",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activation",
+ "columnName": "activation",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "active",
+ "columnName": "active",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_AddressKeyEntity_addressId",
+ "unique": false,
+ "columnNames": [
+ "addressId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_addressId` ON `${TABLE_NAME}` (`addressId`)"
+ },
+ {
+ "name": "index_AddressKeyEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_AddressKeyEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "addressId"
+ ],
+ "referencedColumns": [
+ "addressId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "KeySaltEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `keyId` TEXT NOT NULL, `keySalt` TEXT, PRIMARY KEY(`userId`, `keyId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keyId",
+ "columnName": "keyId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keySalt",
+ "columnName": "keySalt",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "keyId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_KeySaltEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_KeySaltEntity_keyId",
+ "unique": false,
+ "columnNames": [
+ "keyId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_KeySaltEntity_keyId` ON `${TABLE_NAME}` (`keyId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `recipientType` INTEGER NOT NULL, `mimeType` TEXT, `signedKeyList_data` TEXT, `signedKeyList_signature` TEXT, PRIMARY KEY(`email`))",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "recipientType",
+ "columnName": "recipientType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mimeType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.data",
+ "columnName": "signedKeyList_data",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "signedKeyListEntity.signature",
+ "columnName": "signedKeyList_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "PublicAddressKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`email` TEXT NOT NULL, `flags` INTEGER NOT NULL, `publicKey` TEXT NOT NULL, `isPrimary` INTEGER NOT NULL, PRIMARY KEY(`email`, `publicKey`), FOREIGN KEY(`email`) REFERENCES `PublicAddressEntity`(`email`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPrimary",
+ "columnName": "isPrimary",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "email",
+ "publicKey"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_PublicAddressKeyEntity_email",
+ "unique": false,
+ "columnNames": [
+ "email"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_PublicAddressKeyEntity_email` ON `${TABLE_NAME}` (`email`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "PublicAddressEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "email"
+ ],
+ "referencedColumns": [
+ "email"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "HumanVerificationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`clientId` TEXT NOT NULL, `clientIdType` TEXT NOT NULL, `verificationMethods` TEXT NOT NULL, `verificationToken` TEXT, `state` TEXT NOT NULL, `humanHeaderTokenType` TEXT, `humanHeaderTokenCode` TEXT, PRIMARY KEY(`clientId`))",
+ "fields": [
+ {
+ "fieldPath": "clientId",
+ "columnName": "clientId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clientIdType",
+ "columnName": "clientIdType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationMethods",
+ "columnName": "verificationMethods",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "verificationToken",
+ "columnName": "verificationToken",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "humanHeaderTokenType",
+ "columnName": "humanHeaderTokenType",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "humanHeaderTokenCode",
+ "columnName": "humanHeaderTokenCode",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "clientId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "UserSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `news` INTEGER, `locale` TEXT, `logAuth` INTEGER, `invoiceText` TEXT, `density` INTEGER, `theme` TEXT, `themeType` INTEGER, `weekStart` INTEGER, `dateFormat` INTEGER, `timeFormat` INTEGER, `welcome` INTEGER, `earlyAccess` INTEGER, `email_value` TEXT, `email_status` INTEGER, `email_notify` INTEGER, `email_reset` INTEGER, `phone_value` TEXT, `phone_status` INTEGER, `phone_notify` INTEGER, `phone_reset` INTEGER, `password_mode` INTEGER, `password_expirationTime` INTEGER, `twoFA_enabled` INTEGER, `twoFA_allowed` INTEGER, `twoFA_expirationTime` INTEGER, `flags_welcomed` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "news",
+ "columnName": "news",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "locale",
+ "columnName": "locale",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "logAuth",
+ "columnName": "logAuth",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "invoiceText",
+ "columnName": "invoiceText",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "density",
+ "columnName": "density",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "themeType",
+ "columnName": "themeType",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "weekStart",
+ "columnName": "weekStart",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dateFormat",
+ "columnName": "dateFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timeFormat",
+ "columnName": "timeFormat",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "welcome",
+ "columnName": "welcome",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "earlyAccess",
+ "columnName": "earlyAccess",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.value",
+ "columnName": "email_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.status",
+ "columnName": "email_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.notify",
+ "columnName": "email_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email.reset",
+ "columnName": "email_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.value",
+ "columnName": "phone_value",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.status",
+ "columnName": "phone_status",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.notify",
+ "columnName": "phone_notify",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "phone.reset",
+ "columnName": "phone_reset",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.mode",
+ "columnName": "password_mode",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "password.expirationTime",
+ "columnName": "password_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.enabled",
+ "columnName": "twoFA_enabled",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.allowed",
+ "columnName": "twoFA_allowed",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFA.expirationTime",
+ "columnName": "twoFA_expirationTime",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags.welcomed",
+ "columnName": "flags_welcomed",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `name` TEXT NOT NULL, `displayName` TEXT, `planName` TEXT, `vpnPlanName` TEXT, `twoFactorGracePeriod` INTEGER, `theme` TEXT, `email` TEXT, `maxDomains` INTEGER, `maxAddresses` INTEGER, `maxSpace` INTEGER, `maxMembers` INTEGER, `maxVPN` INTEGER, `maxCalendars` INTEGER, `features` INTEGER, `flags` INTEGER, `usedDomains` INTEGER, `usedAddresses` INTEGER, `usedSpace` INTEGER, `assignedSpace` INTEGER, `usedMembers` INTEGER, `usedVPN` INTEGER, `usedCalendars` INTEGER, `hasKeys` INTEGER, `toMigrate` INTEGER, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "planName",
+ "columnName": "planName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "vpnPlanName",
+ "columnName": "vpnPlanName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "twoFactorGracePeriod",
+ "columnName": "twoFactorGracePeriod",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "theme",
+ "columnName": "theme",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "email",
+ "columnName": "email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxDomains",
+ "columnName": "maxDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAddresses",
+ "columnName": "maxAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "maxSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxMembers",
+ "columnName": "maxMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxVPN",
+ "columnName": "maxVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxCalendars",
+ "columnName": "maxCalendars",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "features",
+ "columnName": "features",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedDomains",
+ "columnName": "usedDomains",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedAddresses",
+ "columnName": "usedAddresses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "usedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "assignedSpace",
+ "columnName": "assignedSpace",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedMembers",
+ "columnName": "usedMembers",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedVPN",
+ "columnName": "usedVPN",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedCalendars",
+ "columnName": "usedCalendars",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hasKeys",
+ "columnName": "hasKeys",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "toMigrate",
+ "columnName": "toMigrate",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "OrganizationKeysEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `publicKey` TEXT NOT NULL, `privateKey` TEXT NOT NULL, PRIMARY KEY(`userId`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicKey",
+ "columnName": "publicKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "privateKey",
+ "columnName": "privateKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "EventMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `config` TEXT NOT NULL, `eventId` TEXT, `nextEventId` TEXT, `refresh` TEXT, `more` INTEGER, `response` TEXT, `retry` INTEGER NOT NULL, `state` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER, PRIMARY KEY(`userId`, `config`), FOREIGN KEY(`userId`) REFERENCES `UserEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "config",
+ "columnName": "config",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "eventId",
+ "columnName": "eventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextEventId",
+ "columnName": "nextEventId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "refresh",
+ "columnName": "refresh",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "more",
+ "columnName": "more",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "response",
+ "columnName": "response",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "retry",
+ "columnName": "retry",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "config"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_EventMetadataEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_config",
+ "unique": false,
+ "columnNames": [
+ "config"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_config` ON `${TABLE_NAME}` (`config`)"
+ },
+ {
+ "name": "index_EventMetadataEntity_createdAt",
+ "unique": false,
+ "columnNames": [
+ "createdAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_EventMetadataEntity_createdAt` ON `${TABLE_NAME}` (`createdAt`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UserEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "userId"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FeatureFlagEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` TEXT NOT NULL, `featureId` TEXT NOT NULL, `scope` TEXT NOT NULL, `defaultValue` INTEGER NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`userId`, `featureId`))",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "userId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "featureId",
+ "columnName": "featureId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultValue",
+ "columnName": "defaultValue",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "userId",
+ "featureId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_FeatureFlagEntity_userId",
+ "unique": false,
+ "columnNames": [
+ "userId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_userId` ON `${TABLE_NAME}` (`userId`)"
+ },
+ {
+ "name": "index_FeatureFlagEntity_featureId",
+ "unique": false,
+ "columnNames": [
+ "featureId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_FeatureFlagEntity_featureId` ON `${TABLE_NAME}` (`featureId`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ChallengeFrameEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`challengeFrame` TEXT NOT NULL, `flow` TEXT NOT NULL, `focusTime` TEXT NOT NULL, `clicks` INTEGER NOT NULL, `copy` TEXT NOT NULL, `paste` TEXT NOT NULL, `keys` TEXT NOT NULL, PRIMARY KEY(`challengeFrame`))",
+ "fields": [
+ {
+ "fieldPath": "challengeFrame",
+ "columnName": "challengeFrame",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flow",
+ "columnName": "flow",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTime",
+ "columnName": "focusTime",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "clicks",
+ "columnName": "clicks",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "copy",
+ "columnName": "copy",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paste",
+ "columnName": "paste",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "keys",
+ "columnName": "keys",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "challengeFrame"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "GooglePurchaseEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`googlePurchaseToken` TEXT NOT NULL, `paymentToken` TEXT NOT NULL, PRIMARY KEY(`googlePurchaseToken`))",
+ "fields": [
+ {
+ "fieldPath": "googlePurchaseToken",
+ "columnName": "googlePurchaseToken",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paymentToken",
+ "columnName": "paymentToken",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "googlePurchaseToken"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_GooglePurchaseEntity_paymentToken",
+ "unique": true,
+ "columnNames": [
+ "paymentToken"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_GooglePurchaseEntity_paymentToken` ON `${TABLE_NAME}` (`paymentToken`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "ObservabilityEventEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `data` TEXT NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "timestamp",
+ "columnName": "timestamp",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "data",
+ "columnName": "data",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "VolumeEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `max_space` INTEGER, `used_space` INTEGER NOT NULL, `state` INTEGER NOT NULL, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxSpace",
+ "columnName": "max_space",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "usedSpace",
+ "columnName": "used_space",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_VolumeEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_VolumeEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_VolumeEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `address_id` TEXT, `flags` INTEGER NOT NULL, `link_id` TEXT NOT NULL, `locked` INTEGER NOT NULL, `key` TEXT NOT NULL, `passphrase` TEXT NOT NULL, `passphrase_signature` TEXT NOT NULL, `creation_time` INTEGER, PRIMARY KEY(`user_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "addressId",
+ "columnName": "address_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isLocked",
+ "columnName": "locked",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphrase",
+ "columnName": "passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "passphraseSignature",
+ "columnName": "passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_ShareEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_ShareEntity_id",
+ "unique": true,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_ShareEntity_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "ShareUrlEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `flags` INTEGER NOT NULL, `name` TEXT, `token` TEXT NOT NULL, `creatior_email` TEXT NOT NULL, `permissions` INTEGER NOT NULL, `creation_time` INTEGER NOT NULL, `expiration_time` INTEGER, `last_access_time` INTEGER, `max_accesses` INTEGER, `number_of_accesses` INTEGER NOT NULL, `url_password_salt` TEXT NOT NULL, `share_password_salt` TEXT NOT NULL, `srp_verifier` TEXT NOT NULL, `srp_modulus_id` TEXT NOT NULL, `password` TEXT NOT NULL, `share_passphrase_key_packet` TEXT NOT NULL, `public_url` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "flags",
+ "columnName": "flags",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "token",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creatorEmail",
+ "columnName": "creatior_email",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastAccessTime",
+ "columnName": "last_access_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxAccesses",
+ "columnName": "max_accesses",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "urlPasswordSalt",
+ "columnName": "url_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePasswordSalt",
+ "columnName": "share_password_salt",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpVerifier",
+ "columnName": "srp_verifier",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "srpModulusId",
+ "columnName": "srp_modulus_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedUrlPassword",
+ "columnName": "password",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sharePassphraseKeyPacket",
+ "columnName": "share_passphrase_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "publicUrl",
+ "columnName": "public_url",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_ShareUrlEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_ShareUrlEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_ShareUrlEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ShareUrlEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `parent_id` TEXT, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `name_signature_email` TEXT, `hash` TEXT NOT NULL, `state` INTEGER NOT NULL, `expiration_time` INTEGER, `size` INTEGER NOT NULL, `mime_type` TEXT NOT NULL, `attributes` INTEGER NOT NULL, `permissions` INTEGER NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `signature_address` TEXT NOT NULL, `creation_time` INTEGER NOT NULL, `last_modified` INTEGER NOT NULL, `trashed_time` INTEGER, `is_shared` INTEGER NOT NULL, `number_of_accesses` INTEGER NOT NULL, `share_url_expiration_time` INTEGER, `x_attr` TEXT, `share_url_share_id` TEXT DEFAULT NULL, `share_url_id` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `parent_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nameSignatureEmail",
+ "columnName": "name_signature_email",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "expirationTime",
+ "columnName": "expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attributes",
+ "columnName": "attributes",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "permissions",
+ "columnName": "permissions",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "creationTime",
+ "columnName": "creation_time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "trashedTime",
+ "columnName": "trashed_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shared",
+ "columnName": "is_shared",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "numberOfAccesses",
+ "columnName": "number_of_accesses",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareUrlExpirationTime",
+ "columnName": "share_url_expiration_time",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "xAttr",
+ "columnName": "x_attr",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "shareUrlShareId",
+ "columnName": "share_url_share_id",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "shareUrlId",
+ "columnName": "share_url_id",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ },
+ {
+ "name": "index_LinkEntity_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_id` ON `${TABLE_NAME}` (`id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id` ON `${TABLE_NAME}` (`user_id`, `share_id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_id` ON `${TABLE_NAME}` (`user_id`, `id`)"
+ },
+ {
+ "name": "index_LinkEntity_user_id_share_id_parent_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkEntity_user_id_share_id_parent_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "parent_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFilePropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`file_user_id` TEXT NOT NULL, `file_share_id` TEXT NOT NULL, `file_link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `has_thumbnail` INTEGER NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT, `file_signature_address` TEXT, PRIMARY KEY(`file_user_id`, `file_share_id`, `file_link_id`), FOREIGN KEY(`file_user_id`, `file_share_id`, `file_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "file_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "file_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "file_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "activeRevisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hasThumbnail",
+ "columnName": "has_thumbnail",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "activeRevisionSignatureAddress",
+ "columnName": "file_signature_address",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFilePropertiesEntity_file_share_id",
+ "unique": false,
+ "columnNames": [
+ "file_share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_share_id` ON `${TABLE_NAME}` (`file_share_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_link_id` ON `${TABLE_NAME}` (`file_link_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id",
+ "unique": false,
+ "columnNames": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFilePropertiesEntity_file_user_id_file_share_id_file_link_id` ON `${TABLE_NAME}` (`file_user_id`, `file_share_id`, `file_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "file_user_id",
+ "file_share_id",
+ "file_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkFolderPropertiesEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`folder_user_id` TEXT NOT NULL, `folder_share_id` TEXT NOT NULL, `folder_link_id` TEXT NOT NULL, `node_hash_key` TEXT NOT NULL, PRIMARY KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`), FOREIGN KEY(`folder_user_id`, `folder_share_id`, `folder_link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "folder_user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "folder_share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "folder_link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeHashKey",
+ "columnName": "node_hash_key",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_share_id",
+ "unique": false,
+ "columnNames": [
+ "folder_share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_share_id` ON `${TABLE_NAME}` (`folder_share_id`)"
+ },
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_link_id` ON `${TABLE_NAME}` (`folder_link_id`)"
+ },
+ {
+ "name": "index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id",
+ "unique": false,
+ "columnNames": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkFolderPropertiesEntity_folder_user_id_folder_share_id_folder_link_id` ON `${TABLE_NAME}` (`folder_user_id`, `folder_share_id`, `folder_link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "folder_user_id",
+ "folder_share_id",
+ "folder_link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkOfflineEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkOfflineEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkOfflineEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkOfflineEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_LinkOfflineEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkDownloadStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `state` TEXT NOT NULL, `manifest_signature` TEXT DEFAULT NULL, `signature_address` TEXT DEFAULT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "signatureAddress",
+ "columnName": "signature_address",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkDownloadStateEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_state",
+ "unique": false,
+ "columnNames": [
+ "state"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_state` ON `${TABLE_NAME}` (`state`)"
+ },
+ {
+ "name": "index_LinkDownloadStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkDownloadStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DownloadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `index` INTEGER NOT NULL, `uri` TEXT NOT NULL, `encrypted_signature` TEXT, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `revision_id`, `index`), FOREIGN KEY(`user_id`, `share_id`, `link_id`, `revision_id`) REFERENCES `LinkDownloadStateEntity`(`user_id`, `share_id`, `link_id`, `revision_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DownloadBlockEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_DownloadBlockEntity_user_id_share_id_link_id_revision_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DownloadBlockEntity_user_id_share_id_link_id_revision_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`, `revision_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkDownloadStateEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "revision_id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkTrashStateEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `state` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkTrashStateEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_state",
+ "unique": false,
+ "columnNames": [
+ "state"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_state` ON `${TABLE_NAME}` (`state`)"
+ },
+ {
+ "name": "index_LinkTrashStateEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkTrashStateEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashWorkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `work_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "workId",
+ "columnName": "work_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_TrashWorkEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_TrashWorkEntity_work_id",
+ "unique": false,
+ "columnNames": [
+ "work_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_TrashWorkEntity_work_id` ON `${TABLE_NAME}` (`work_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "MessageEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `content` TEXT NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_MessageEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_MessageEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UiSettingsEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `layout_type` TEXT NOT NULL, `theme_style` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "layoutType",
+ "columnName": "layout_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "themeStyle",
+ "columnName": "theme_style",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "DriveLinkRemoteKeyEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `user_id` TEXT NOT NULL, `previous_key` INTEGER, `next_key` INTEGER, PRIMARY KEY(`key`, `user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "prevKey",
+ "columnName": "previous_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "nextKey",
+ "columnName": "next_key",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "key",
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id",
+ "unique": true,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_DriveLinkRemoteKeyEntity_key",
+ "unique": false,
+ "columnNames": [
+ "key"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_DriveLinkRemoteKeyEntity_key` ON `${TABLE_NAME}` (`key`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ },
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "SortingEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `sorting_by` TEXT NOT NULL, `sorting_direction` TEXT NOT NULL, PRIMARY KEY(`user_id`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingBy",
+ "columnName": "sorting_by",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortingDirection",
+ "columnName": "sorting_direction",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkUploadEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `revision_id` TEXT NOT NULL, `name` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `node_key` TEXT NOT NULL, `node_passphrase` TEXT NOT NULL, `node_passphrase_signature` TEXT NOT NULL, `content_key_packet` TEXT NOT NULL, `content_key_packet_signature` TEXT NOT NULL, `manifest_signature` TEXT NOT NULL, `state` TEXT NOT NULL, `size` INTEGER DEFAULT NULL, `last_modified` INTEGER, `uri` TEXT DEFAULT NULL, `should_delete_source_uri` INTEGER NOT NULL DEFAULT false, `media_resolution_width` INTEGER DEFAULT NULL, `media_resolution_height` INTEGER DEFAULT NULL, `digests` TEXT DEFAULT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "revisionId",
+ "columnName": "revision_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "mimeType",
+ "columnName": "mime_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodeKey",
+ "columnName": "node_key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphrase",
+ "columnName": "node_passphrase",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "nodePassphraseSignature",
+ "columnName": "node_passphrase_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacket",
+ "columnName": "content_key_packet",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentKeyPacketSignature",
+ "columnName": "content_key_packet_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "manifestSignature",
+ "columnName": "manifest_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "state",
+ "columnName": "state",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "lastModified",
+ "columnName": "last_modified",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "shouldDeleteSourceUri",
+ "columnName": "should_delete_source_uri",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "false"
+ },
+ {
+ "fieldPath": "mediaResolutionWidth",
+ "columnName": "media_resolution_width",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "mediaResolutionHeight",
+ "columnName": "media_resolution_height",
+ "affinity": "INTEGER",
+ "notNull": false,
+ "defaultValue": "NULL"
+ },
+ {
+ "fieldPath": "digests",
+ "columnName": "digests",
+ "affinity": "TEXT",
+ "notNull": false,
+ "defaultValue": "NULL"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_LinkUploadEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_revision_id",
+ "unique": false,
+ "columnNames": [
+ "revision_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_revision_id` ON `${TABLE_NAME}` (`revision_id`)"
+ },
+ {
+ "name": "index_LinkUploadEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkUploadEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBlockEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_link_id` INTEGER NOT NULL, `index` INTEGER NOT NULL, `size` INTEGER NOT NULL, `encrypted_signature` TEXT NOT NULL, `hash` TEXT NOT NULL, `token` TEXT NOT NULL, `url` TEXT NOT NULL, `raw_size` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`upload_link_id`, `index`), FOREIGN KEY(`upload_link_id`) REFERENCES `LinkUploadEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "uploadLinkId",
+ "columnName": "upload_link_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "size",
+ "columnName": "size",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "encryptedSignature",
+ "columnName": "encrypted_signature",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "hash",
+ "columnName": "hash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uploadToken",
+ "columnName": "token",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rawSize",
+ "columnName": "raw_size",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_link_id",
+ "index"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkUploadEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_link_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBulkEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `user_id` TEXT NOT NULL, `volume_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `parent_id` TEXT NOT NULL, `should_delete_source_uri` INTEGER NOT NULL, FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "volumeId",
+ "columnName": "volume_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentId",
+ "columnName": "parent_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shouldDeleteSourceUri",
+ "columnName": "should_delete_source_uri",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_UploadBulkEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_volume_id",
+ "unique": false,
+ "columnNames": [
+ "volume_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_volume_id` ON `${TABLE_NAME}` (`volume_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_UploadBulkEntity_parent_id",
+ "unique": false,
+ "columnNames": [
+ "parent_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkEntity_parent_id` ON `${TABLE_NAME}` (`parent_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "UploadBulkUriStringEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`upload_bulk_id` INTEGER NOT NULL, `uri` TEXT NOT NULL, PRIMARY KEY(`upload_bulk_id`, `uri`), FOREIGN KEY(`upload_bulk_id`) REFERENCES `UploadBulkEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "upload_bulk_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "uri",
+ "columnName": "uri",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "upload_bulk_id",
+ "uri"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_UploadBulkUriStringEntity_upload_bulk_id",
+ "unique": false,
+ "columnNames": [
+ "upload_bulk_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_UploadBulkUriStringEntity_upload_bulk_id` ON `${TABLE_NAME}` (`upload_bulk_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "UploadBulkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "upload_bulk_id"
+ ],
+ "referencedColumns": [
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "FolderMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `last_fetch_children_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`, `link_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchChildrenTimestamp",
+ "columnName": "last_fetch_children_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "TrashMetadataEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `last_fetch_trash_timestamp` INTEGER, PRIMARY KEY(`user_id`, `share_id`), FOREIGN KEY(`user_id`, `share_id`) REFERENCES `ShareEntity`(`user_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastFetchTrashTimestamp",
+ "columnName": "last_fetch_trash_timestamp",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": [
+ {
+ "table": "ShareEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "id"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NotificationChannelEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`user_id`, `type`), FOREIGN KEY(`user_id`) REFERENCES `AccountEntity`(`userId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "type"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_NotificationChannelEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationChannelEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "AccountEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id"
+ ],
+ "referencedColumns": [
+ "userId"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "NotificationEventEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `channel_type` TEXT NOT NULL, `notification_tag` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, `notification_event_id` TEXT NOT NULL, `notification_event` TEXT NOT NULL, PRIMARY KEY(`user_id`, `channel_type`, `notification_tag`, `notification_id`, `notification_event_id`), FOREIGN KEY(`user_id`, `channel_type`) REFERENCES `NotificationChannelEntity`(`user_id`, `type`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "channelType",
+ "columnName": "channel_type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationTag",
+ "columnName": "notification_tag",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationId",
+ "columnName": "notification_id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationEventId",
+ "columnName": "notification_event_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationEvent",
+ "columnName": "notification_event",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "channel_type",
+ "notification_tag",
+ "notification_id",
+ "notification_event_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "channel_type",
+ "notification_tag",
+ "notification_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEventEntity_user_id_channel_type_notification_tag_notification_id` ON `${TABLE_NAME}` (`user_id`, `channel_type`, `notification_tag`, `notification_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "NotificationChannelEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "channel_type"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "type"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "LinkSelectionEntity",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`user_id` TEXT NOT NULL, `share_id` TEXT NOT NULL, `link_id` TEXT NOT NULL, `selection_id` TEXT NOT NULL, PRIMARY KEY(`user_id`, `share_id`, `link_id`, `selection_id`), FOREIGN KEY(`user_id`, `share_id`, `link_id`) REFERENCES `LinkEntity`(`user_id`, `share_id`, `id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "userId",
+ "columnName": "user_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "shareId",
+ "columnName": "share_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "linkId",
+ "columnName": "link_id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "selectionId",
+ "columnName": "selection_id",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id",
+ "selection_id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_LinkSelectionEntity_user_id_share_id_link_id",
+ "unique": false,
+ "columnNames": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id_share_id_link_id` ON `${TABLE_NAME}` (`user_id`, `share_id`, `link_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_user_id",
+ "unique": false,
+ "columnNames": [
+ "user_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_user_id` ON `${TABLE_NAME}` (`user_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_share_id",
+ "unique": false,
+ "columnNames": [
+ "share_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_share_id` ON `${TABLE_NAME}` (`share_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_link_id",
+ "unique": false,
+ "columnNames": [
+ "link_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_link_id` ON `${TABLE_NAME}` (`link_id`)"
+ },
+ {
+ "name": "index_LinkSelectionEntity_selection_id",
+ "unique": false,
+ "columnNames": [
+ "selection_id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_LinkSelectionEntity_selection_id` ON `${TABLE_NAME}` (`selection_id`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "LinkEntity",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "user_id",
+ "share_id",
+ "link_id"
+ ],
+ "referencedColumns": [
+ "user_id",
+ "share_id",
+ "id"
+ ]
+ }
+ ]
+ }
+ ],
+ "views": [],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '59591b661e49ae97591acd9c77f82223')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/3.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/3.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/3.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/3.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/4.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/4.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/4.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/4.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/5.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/5.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/5.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/5.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/6.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/6.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/6.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/6.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/7.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/7.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/7.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/7.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/8.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/8.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/8.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/8.json
diff --git a/app/schemas/me.proton.android.drive.db.AppDatabase/9.json b/drive/db/schemas/me.proton.android.drive.db.DriveDatabase/9.json
similarity index 100%
rename from app/schemas/me.proton.android.drive.db.AppDatabase/9.json
rename to drive/db/schemas/me.proton.android.drive.db.DriveDatabase/9.json
diff --git a/drive/db/src/main/AndroidManifest.xml b/drive/db/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..35ad1b9a
--- /dev/null
+++ b/drive/db/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+
+
+
\ No newline at end of file
diff --git a/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt
new file mode 100644
index 00000000..836b8db6
--- /dev/null
+++ b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabase.kt
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.android.drive.db
+
+import android.content.Context
+import androidx.room.AutoMigration
+import androidx.room.Database
+import androidx.room.TypeConverters
+import me.proton.core.account.data.db.AccountConverters
+import me.proton.core.account.data.db.AccountDatabase
+import me.proton.core.account.data.entity.AccountEntity
+import me.proton.core.account.data.entity.AccountMetadataEntity
+import me.proton.core.account.data.entity.SessionDetailsEntity
+import me.proton.core.account.data.entity.SessionEntity
+import me.proton.core.challenge.data.db.ChallengeConverters
+import me.proton.core.challenge.data.db.ChallengeDatabase
+import me.proton.core.challenge.data.entity.ChallengeFrameEntity
+import me.proton.core.crypto.android.keystore.CryptoConverters
+import me.proton.core.data.room.db.BaseDatabase
+import me.proton.core.data.room.db.CommonConverters
+import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase
+import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase
+import me.proton.core.drive.drivelink.offline.data.db.DriveLinkOfflineDatabase
+import me.proton.core.drive.drivelink.paged.data.db.DriveLinkPagedDatabase
+import me.proton.core.drive.drivelink.paged.data.db.entity.DriveLinkRemoteKeyEntity
+import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase
+import me.proton.core.drive.drivelink.shared.data.db.DriveLinkSharedDatabase
+import me.proton.core.drive.drivelink.trash.data.db.DriveLinkTrashDatabase
+import me.proton.core.drive.folder.data.db.FolderDatabase
+import me.proton.core.drive.folder.data.db.FolderMetadataEntity
+import me.proton.core.drive.link.data.db.LinkDatabase
+import me.proton.core.drive.link.data.db.entity.LinkEntity
+import me.proton.core.drive.link.data.db.entity.LinkFilePropertiesEntity
+import me.proton.core.drive.link.data.db.entity.LinkFolderPropertiesEntity
+import me.proton.core.drive.link.selection.data.db.LinkSelectionConverters
+import me.proton.core.drive.link.selection.data.db.LinkSelectionDatabase
+import me.proton.core.drive.link.selection.data.db.entity.LinkSelectionEntity
+import me.proton.core.drive.linkdownload.data.db.LinkDownloadDatabase
+import me.proton.core.drive.linkdownload.data.db.entity.DownloadBlockEntity
+import me.proton.core.drive.linkdownload.data.db.entity.LinkDownloadStateEntity
+import me.proton.core.drive.linknode.data.db.LinkAncestorDatabase
+import me.proton.core.drive.linkoffline.data.db.LinkOfflineDatabase
+import me.proton.core.drive.linkoffline.data.db.LinkOfflineEntity
+import me.proton.core.drive.linktrash.data.db.LinkTrashDatabase
+import me.proton.core.drive.linktrash.data.db.entity.LinkTrashStateEntity
+import me.proton.core.drive.linktrash.data.db.entity.TrashMetadataEntity
+import me.proton.core.drive.linktrash.data.db.entity.TrashWorkEntity
+import me.proton.core.drive.linkupload.data.db.LinkUploadDatabase
+import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity
+import me.proton.core.drive.linkupload.data.db.entity.UploadBlockEntity
+import me.proton.core.drive.linkupload.data.db.entity.UploadBulkEntity
+import me.proton.core.drive.linkupload.data.db.entity.UploadBulkUriStringEntity
+import me.proton.core.drive.messagequeue.data.storage.db.MessageQueueDatabase
+import me.proton.core.drive.messagequeue.data.storage.db.entity.MessageEntity
+import me.proton.core.drive.notification.data.db.NotificationConverters
+import me.proton.core.drive.notification.data.db.NotificationDatabase
+import me.proton.core.drive.notification.data.db.entity.NotificationChannelEntity
+import me.proton.core.drive.notification.data.db.entity.NotificationEventEntity
+import me.proton.core.drive.share.data.db.ShareDatabase
+import me.proton.core.drive.share.data.db.ShareEntity
+import me.proton.core.drive.shareurl.base.data.db.ShareUrlDatabase
+import me.proton.core.drive.shareurl.base.data.db.entity.ShareUrlEntity
+import me.proton.core.drive.sorting.data.db.SortingDatabase
+import me.proton.core.drive.sorting.data.db.entity.SortingEntity
+import me.proton.core.drive.volume.data.db.VolumeDatabase
+import me.proton.core.drive.volume.data.db.VolumeEntity
+import me.proton.core.eventmanager.data.db.EventManagerConverters
+import me.proton.core.eventmanager.data.db.EventMetadataDatabase
+import me.proton.core.eventmanager.data.entity.EventMetadataEntity
+import me.proton.core.featureflag.data.db.FeatureFlagDatabase
+import me.proton.core.featureflag.data.entity.FeatureFlagEntity
+import me.proton.core.humanverification.data.db.HumanVerificationConverters
+import me.proton.core.humanverification.data.db.HumanVerificationDatabase
+import me.proton.core.humanverification.data.entity.HumanVerificationEntity
+import me.proton.core.key.data.db.KeySaltDatabase
+import me.proton.core.key.data.db.PublicAddressDatabase
+import me.proton.core.key.data.entity.KeySaltEntity
+import me.proton.core.key.data.entity.PublicAddressEntity
+import me.proton.core.key.data.entity.PublicAddressKeyEntity
+import me.proton.core.observability.data.db.ObservabilityDatabase
+import me.proton.core.observability.data.entity.ObservabilityEventEntity
+import me.proton.core.payment.data.local.db.PaymentDatabase
+import me.proton.core.payment.data.local.entity.GooglePurchaseEntity
+import me.proton.core.user.data.db.AddressDatabase
+import me.proton.core.user.data.db.UserConverters
+import me.proton.core.user.data.db.UserDatabase
+import me.proton.core.user.data.entity.AddressEntity
+import me.proton.core.user.data.entity.AddressKeyEntity
+import me.proton.core.user.data.entity.UserEntity
+import me.proton.core.user.data.entity.UserKeyEntity
+import me.proton.core.usersettings.data.db.OrganizationDatabase
+import me.proton.core.usersettings.data.db.UserSettingsConverters
+import me.proton.core.usersettings.data.db.UserSettingsDatabase
+import me.proton.core.usersettings.data.entity.OrganizationEntity
+import me.proton.core.usersettings.data.entity.OrganizationKeysEntity
+import me.proton.core.usersettings.data.entity.UserSettingsEntity
+import me.proton.drive.android.settings.data.db.AppUiSettingsDatabase
+import me.proton.drive.android.settings.data.db.entity.UiSettingsEntity
+
+@Database(
+ entities = [
+ // Core
+ AccountEntity::class,
+ AccountMetadataEntity::class,
+ SessionEntity::class,
+ SessionDetailsEntity::class,
+ UserEntity::class,
+ UserKeyEntity::class,
+ AddressEntity::class,
+ AddressKeyEntity::class,
+ KeySaltEntity::class,
+ PublicAddressEntity::class,
+ PublicAddressKeyEntity::class,
+ HumanVerificationEntity::class,
+ UserSettingsEntity::class,
+ OrganizationEntity::class,
+ OrganizationKeysEntity::class,
+ EventMetadataEntity::class,
+ FeatureFlagEntity::class,
+ ChallengeFrameEntity::class,
+ GooglePurchaseEntity::class,
+ ObservabilityEventEntity::class,
+ // Drive
+ VolumeEntity::class,
+ ShareEntity::class,
+ ShareUrlEntity::class,
+ LinkEntity::class,
+ LinkFilePropertiesEntity::class,
+ LinkFolderPropertiesEntity::class,
+ LinkOfflineEntity::class,
+ LinkDownloadStateEntity::class,
+ DownloadBlockEntity::class,
+ LinkTrashStateEntity::class,
+ // Trash
+ TrashWorkEntity::class,
+ // MessageQueue
+ MessageEntity::class,
+ // AppUiSettings
+ UiSettingsEntity::class,
+ // DriveLinkPaged
+ DriveLinkRemoteKeyEntity::class,
+ // Sorting
+ SortingEntity::class,
+ // Upload
+ LinkUploadEntity::class,
+ UploadBlockEntity::class,
+ UploadBulkEntity::class,
+ UploadBulkUriStringEntity::class,
+ FolderMetadataEntity::class,
+ TrashMetadataEntity::class,
+ // Notification
+ NotificationChannelEntity::class,
+ NotificationEventEntity::class,
+ // Selection
+ LinkSelectionEntity::class,
+ ],
+ version = DriveDatabase.VERSION,
+ autoMigrations = [
+ AutoMigration(from = 4, to = 5),
+ AutoMigration(from = 5, to = 6),
+ AutoMigration(from = 7, to = 8),
+ AutoMigration(from = 9, to = 10),
+ AutoMigration(from = 13, to = 14),
+ AutoMigration(from = 15, to = 16),
+ AutoMigration(from = 16, to = 17),
+ AutoMigration(from = 17, to = 18, spec = ShareDatabase.DeleteBlockSizeFromShareEntity::class),
+ AutoMigration(from = 18, to = 19),
+ AutoMigration(from = 22, to = 23),
+ ],
+ exportSchema = true,
+)
+@TypeConverters(
+ // Core
+ CommonConverters::class,
+ AccountConverters::class,
+ UserConverters::class,
+ CryptoConverters::class,
+ HumanVerificationConverters::class,
+ UserSettingsConverters::class,
+ EventManagerConverters::class,
+ ChallengeConverters::class,
+ // Drive
+ NotificationConverters::class,
+ LinkSelectionConverters::class,
+)
+abstract class DriveDatabase :
+ BaseDatabase(),
+ AccountDatabase,
+ UserDatabase,
+ AddressDatabase,
+ KeySaltDatabase,
+ HumanVerificationDatabase,
+ PublicAddressDatabase,
+ UserSettingsDatabase,
+ OrganizationDatabase,
+ FeatureFlagDatabase,
+ VolumeDatabase,
+ ShareDatabase,
+ ShareUrlDatabase,
+ LinkDatabase,
+ FolderDatabase,
+ LinkAncestorDatabase,
+ LinkOfflineDatabase,
+ LinkDownloadDatabase,
+ LinkTrashDatabase,
+ LinkSelectionDatabase,
+ MessageQueueDatabase,
+ AppUiSettingsDatabase,
+ EventMetadataDatabase,
+ ChallengeDatabase,
+ SortingDatabase,
+ LinkUploadDatabase,
+ DriveLinkDatabase,
+ DriveLinkPagedDatabase,
+ DriveLinkTrashDatabase,
+ DriveLinkOfflineDatabase,
+ DriveLinkDownloadDatabase,
+ DriveLinkSharedDatabase,
+ DriveLinkSelectionDatabase,
+ NotificationDatabase,
+ PaymentDatabase,
+ ObservabilityDatabase {
+
+ companion object {
+ const val VERSION = 23
+
+ private val migrations = listOf(
+ DriveDatabaseMigrations.MIGRATION_1_2,
+ DriveDatabaseMigrations.MIGRATION_2_3,
+ DriveDatabaseMigrations.MIGRATION_3_4,
+ //AutoMigration(from = 4, to = 5)
+ //AutoMigration(from = 5, to = 6)
+ DriveDatabaseMigrations.MIGRATION_6_7,
+ //AutoMigration(from = 7, to = 8)
+ DriveDatabaseMigrations.MIGRATION_8_9,
+ //AutoMigration(from = 9, to = 10)
+ DriveDatabaseMigrations.MIGRATION_10_11,
+ DriveDatabaseMigrations.MIGRATION_11_12,
+ DriveDatabaseMigrations.MIGRATION_12_13,
+ //AutoMigration(from = 13, to = 14)
+ DriveDatabaseMigrations.MIGRATION_14_15,
+ //AutoMigration(from = 15, to = 16)
+ //AutoMigration(from = 16, to = 17)
+ //AutoMigration(from = 17, to = 18)
+ //AutoMigration(from = 18, to = 19)
+ DriveDatabaseMigrations.MIGRATION_19_20,
+ DriveDatabaseMigrations.MIGRATION_20_21,
+ DriveDatabaseMigrations.MIGRATION_21_22,
+ //AutoMigration(from = 22, to = 23)
+ )
+
+ fun buildDatabase(context: Context): DriveDatabase =
+ databaseBuilder(context, "db-drive")
+ .apply { migrations.forEach { addMigrations(it) } }
+ .build()
+ }
+}
diff --git a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt
similarity index 87%
rename from app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt
rename to drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt
index d71b5a8e..7e271812 100644
--- a/app/src/main/kotlin/me/proton/android/drive/db/AppDatabaseMigrations.kt
+++ b/drive/db/src/main/kotlin/me/proton/android/drive/db/DriveDatabaseMigrations.kt
@@ -1,19 +1,19 @@
/*
* Copyright (c) 2023 Proton AG.
- * This file is part of Proton Drive.
+ * This file is part of Proton Core.
*
- * Proton Drive is free software: you can redistribute it and/or modify
+ * Proton Core is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
- * Proton Drive is distributed in the hope that it will be useful,
+ * Proton Core 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 Proton Drive. If not, see .
+ * along with Proton Core. If not, see .
*/
package me.proton.android.drive.db
@@ -28,11 +28,12 @@ import me.proton.core.drive.notification.data.db.NotificationDatabase
import me.proton.core.drive.share.data.db.ShareDatabase
import me.proton.core.featureflag.data.db.FeatureFlagDatabase
import me.proton.core.humanverification.data.db.HumanVerificationDatabase
+import me.proton.core.observability.data.db.ObservabilityDatabase
import me.proton.core.payment.data.local.db.PaymentDatabase
import me.proton.core.usersettings.data.db.OrganizationDatabase
import me.proton.core.usersettings.data.db.UserSettingsDatabase
-object AppDatabaseMigrations {
+object DriveDatabaseMigrations {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
NotificationDatabase.MIGRATION_0.migrate(database)
@@ -104,4 +105,9 @@ object AppDatabaseMigrations {
}
}
+ val MIGRATION_21_22 = object : Migration(21, 22) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ ObservabilityDatabase.MIGRATION_0.migrate(database)
+ }
+ }
}
diff --git a/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt b/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt
index 98587e97..62117be0 100644
--- a/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt
+++ b/drive/documentsprovider/domain/src/main/kotlin/me/proton/core/drive/documentsprovider/domain/usecase/GetDocumentsProviderRoots.kt
@@ -18,14 +18,8 @@
package me.proton.core.drive.documentsprovider.domain.usecase
-import kotlinx.coroutines.flow.first
-import me.proton.core.accountmanager.domain.AccountManager
-import javax.inject.Inject
+import me.proton.core.account.domain.entity.Account
-class GetDocumentsProviderRoots @Inject constructor(
- private val accountManager: AccountManager,
-) {
-
- suspend operator fun invoke() =
- accountManager.getAccounts().first()
+interface GetDocumentsProviderRoots {
+ suspend operator fun invoke(): List
}
diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt
index a425cba3..4447f00d 100644
--- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt
+++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDecryptedDriveLinks.kt
@@ -34,4 +34,10 @@ class GetDecryptedDriveLinks @Inject constructor(
.mapCatching { driveLinks ->
decryptDriveLinks(driveLinks)
}
+
+ operator fun invoke(parentId: FolderId, fromIndex: Int, count: Int,): Flow>> =
+ getFolderChildrenDriveLinks(parentId, fromIndex, count)
+ .mapCatching { driveLinks ->
+ decryptDriveLinks(driveLinks)
+ }
}
diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt
index c756c8e6..731b3182 100644
--- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt
+++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetDriveLinks.kt
@@ -33,4 +33,8 @@ class GetDriveLinks @Inject constructor(
operator fun invoke(parentId: FolderId): Flow> =
repository.getDriveLinks(parentId)
.map { driveLinks -> updateIsAnyAncestorMarkedAsOffline(driveLinks) }
+
+ operator fun invoke(parentId: FolderId, fromIndex: Int, count: Int): Flow> =
+ repository.getDriveLinks(parentId, fromIndex, count)
+ .map { driveLinks -> updateIsAnyAncestorMarkedAsOffline(driveLinks) }
}
diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt
index a8ba0a2f..2a77a89d 100644
--- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt
+++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetFolderChildrenDriveLinks.kt
@@ -59,4 +59,25 @@ class GetFolderChildrenDriveLinks @Inject constructor(
}
emitAll(getDriveLinks(folderId).map { driveLinks -> driveLinks.asSuccess })
}
+
+ operator fun invoke(
+ folderId: FolderId,
+ fromIndex: Int,
+ count: Int,
+ refresh: Flow = flowOf { folderRepository.shouldInitiallyFetchFolderChildren(folderId) }
+ ): Flow>> =
+ refresh.transform { shouldRefresh ->
+ if (shouldRefresh) {
+ fetcher> {
+ val (_, saveAction) = folderRepository.fetchFolderChildren(
+ folderId = folderId,
+ pageIndex = 0,
+ pageSize = configurationProvider.uiPageSize,
+ sorting = getSorting(folderId.userId).first().toFolderSorting()
+ ).getOrThrow()
+ saveAction()
+ }
+ }
+ emitAll(getDriveLinks(folderId, fromIndex, count).map { driveLinks -> driveLinks.asSuccess })
+ }
}
diff --git a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt
index bc0f9282..778f222c 100644
--- a/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt
+++ b/drive/drivelink-list/domain/src/main/kotlin/me/proton/core/drive/drivelink/list/domain/usecase/GetPagedDriveLinksList.kt
@@ -27,6 +27,7 @@ import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.domain.extension.mapCatching
import me.proton.core.drive.drivelink.crypto.domain.usecase.DecryptDriveLinks
+import me.proton.core.drive.drivelink.domain.usecase.GetDriveLinksCount
import me.proton.core.drive.drivelink.paged.domain.usecase.GetPagedDriveLinks
import me.proton.core.drive.drivelink.sorting.domain.usecase.SortDriveLinks
import me.proton.core.drive.link.domain.entity.FolderId
@@ -42,6 +43,7 @@ class GetPagedDriveLinksList @Inject constructor(
private val getMainShare: GetMainShare,
private val getPagedDriveLinks: GetPagedDriveLinks,
private val getDecryptedDriveLinks: GetDecryptedDriveLinks,
+ private val getDriveLinksCount: GetDriveLinksCount,
private val getFolderChildrenDriveLinks: GetFolderChildrenDriveLinks,
private val fetchDriveLinksListPage: FetchDriveLinksListPage,
private val getSorting: GetSorting,
@@ -74,22 +76,23 @@ class GetPagedDriveLinksList @Inject constructor(
pageSize
)
},
- localDriveLinks = {
+ localPagedDriveLinks = { fromIndex, count ->
if (sorting.by == By.NAME || sorting.by == By.LAST_MODIFIED) {
- getDecryptedDriveLinks(folderId)
+ getDecryptedDriveLinks(folderId, fromIndex, count)
.mapCatching { driveLinks ->
sortDriveLinks(sorting, driveLinks)
}
} else {
- getFolderChildrenDriveLinks(folderId)
+ getFolderChildrenDriveLinks(folderId, fromIndex, count)
.mapCatching { driveLinks ->
sortDriveLinks(sorting, driveLinks)
}
}
},
+ localDriveLinksCount = { getDriveLinksCount(parentId = folderId) },
processPage = takeIf { sorting.by != By.NAME && sorting.by != By.LAST_MODIFIED }?.let {
{ page -> decryptDriveLinks(page) }
- }
+ },
)
}
}
diff --git a/drive/drivelink-paged/domain/build.gradle.kts b/drive/drivelink-paged/domain/build.gradle.kts
index 4040eab7..887f35d3 100644
--- a/drive/drivelink-paged/domain/build.gradle.kts
+++ b/drive/drivelink-paged/domain/build.gradle.kts
@@ -22,5 +22,5 @@ plugins {
driveModule(hilt = true) {
api(project(":drive:drivelink:domain"))
api(libs.androidx.paging.common)
- implementation(project(":drive:base:data"))
+ implementation(project(":drive:base:data")) // use for asPagingSource
}
diff --git a/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt
new file mode 100644
index 00000000..1783fe40
--- /dev/null
+++ b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetObservablePageSize.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.drivelink.paged.domain.usecase
+
+import me.proton.core.drive.base.domain.extension.MiB
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
+import me.proton.core.drive.base.domain.usecase.GetMemoryInfo
+import javax.inject.Inject
+
+class GetObservablePageSize @Inject constructor(
+ private val getMemoryInfo: GetMemoryInfo,
+ private val configurationProvider: ConfigurationProvider,
+) {
+ operator fun invoke(): Int {
+ val memoryInfo = getMemoryInfo().getOrNull()
+ return if (memoryInfo == null || memoryInfo.isLowOnMemory) {
+ configurationProvider.dbPageSize
+ } else {
+ val multiplier = when {
+ memoryInfo.memoryClass > 512.MiB -> 4
+ memoryInfo.memoryClass > 256.MiB -> 3
+ memoryInfo.memoryClass > 128.MiB -> 2
+ else -> 1
+ }
+ configurationProvider.dbPageSize * multiplier
+ }
+ }
+}
diff --git a/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt
index fb43446d..9b6b7b8b 100644
--- a/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt
+++ b/drive/drivelink-paged/domain/src/main/kotlin/me/proton/core/drive/drivelink/paged/domain/usecase/GetPagedDriveLinks.kt
@@ -34,6 +34,7 @@ import javax.inject.Inject
class GetPagedDriveLinks @Inject constructor(
private val factory: DriveLinkRemoteMediatorFactory,
private val configurationProvider: ConfigurationProvider,
+ private val getObservablePageSize: GetObservablePageSize,
) {
@OptIn(ExperimentalPagingApi::class)
@@ -55,4 +56,33 @@ class GetPagedDriveLinks @Inject constructor(
pagingSourceFactory = { localDriveLinks().asPagingSource(processPage = processPage) }
).flow
}
+
+ @OptIn(ExperimentalPagingApi::class)
+ operator fun invoke(
+ userId: UserId,
+ pagedListKey: String,
+ remoteDriveLinks: suspend (page: Int, pageSize: Int) -> Result,
+ localPagedDriveLinks: (Int, Int) -> Flow>>,
+ localDriveLinksCount: () -> Flow,
+ pageSize: Int = configurationProvider.uiPageSize,
+ processPage: (suspend (List) -> List)? = null,
+ ): Flow> {
+ return Pager(
+ PagingConfig(
+ pageSize = pageSize,
+ initialLoadSize = pageSize,
+ enablePlaceholders = false,
+ ),
+ remoteMediator = factory.create(userId, pagedListKey, remoteDriveLinks),
+ pagingSourceFactory = {
+ { fromIndex: Int, count: Int ->
+ localPagedDriveLinks(fromIndex, count)
+ }.asPagingSource(
+ sourceSize = localDriveLinksCount(),
+ observablePageSize = getObservablePageSize(),
+ processPage = processPage,
+ )
+ }
+ ).flow
+ }
}
diff --git a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt
index 6d2ccce0..2ae9616f 100644
--- a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt
+++ b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/db/dao/DriveLinkSelectionDao.kt
@@ -25,6 +25,7 @@ import me.proton.core.drive.drivelink.data.db.dao.DriveLinkDao
import me.proton.core.drive.drivelink.data.db.entity.DriveLinkEntityWithBlock
import me.proton.core.drive.drivelink.data.db.entity.DriveLinkEntityWithBlock.Companion.SELECTION_PREFIX
import me.proton.core.drive.link.selection.domain.entity.SelectionId
+import me.proton.core.drive.linktrash.data.db.dao.LinkTrashDao
@Dao
interface DriveLinkSelectionDao : DriveLinkDao {
@@ -32,7 +33,7 @@ interface DriveLinkSelectionDao : DriveLinkDao {
@Query(
"""
SELECT ${DriveLinkDao.DRIVE_LINK_SELECT} FROM ${DriveLinkDao.DRIVE_LINK_ENTITY}
- WHERE ${SELECTION_PREFIX}_${Column.SELECTION_ID} = :selectionId
+ WHERE ${SELECTION_PREFIX}_${Column.SELECTION_ID} = :selectionId AND ${LinkTrashDao.NOT_TRASHED_CONDITION}
"""
)
fun getSelectedLinks(selectionId: SelectionId): Flow>
diff --git a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt
index 2ea47367..bc23f7d4 100644
--- a/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt
+++ b/drive/drivelink-selection/data/src/main/kotlin/me/proton/core/drive/drivelink/selection/data/repository/DriveLinkSelectionRepositoryImpl.kt
@@ -18,20 +18,54 @@
package me.proton.core.drive.drivelink.selection.data.repository
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
+import me.proton.core.drive.base.domain.provider.ConfigurationProvider
import me.proton.core.drive.drivelink.data.extension.toDriveLinks
import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.selection.data.db.DriveLinkSelectionDatabase
import javax.inject.Inject
import me.proton.core.drive.drivelink.selection.domain.repository.DriveLinkSelectionRepository
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.link.domain.entity.LinkId
import me.proton.core.drive.link.selection.domain.entity.SelectionId
class DriveLinkSelectionRepositoryImpl @Inject constructor(
private val db: DriveLinkSelectionDatabase,
+ private val configurationProvider: ConfigurationProvider,
) : DriveLinkSelectionRepository {
override fun getSelectedDriveLinks(selectionId: SelectionId): Flow> =
db.driveLinkSelectionDao.getSelectedLinks(selectionId).map { entities ->
entities.toDriveLinks()
}
+
+ override suspend fun selectAll(
+ parentId: FolderId,
+ selectionId: SelectionId?,
+ getDriveLinks: (fromIndex: Int, count: Int) -> Flow>,
+ selectLinks: suspend (SelectionId?, List) -> Result,
+ ) {
+ val pageSize = configurationProvider.dbPageSize
+ var id = selectionId
+ db.inTransaction {
+ var pageIndex = 0
+ while (true) {
+ val driveLinks = getDriveLinks(
+ pageIndex++ * pageSize,
+ pageSize,
+ )
+ .map { driveLinks -> driveLinks.map { driveLink -> driveLink.id } }
+ .first()
+ if (driveLinks.isNotEmpty()) {
+ id = selectLinks(
+ id,
+ driveLinks,
+ ).getOrNull()
+ } else {
+ break
+ }
+ }
+ }
+ }
}
diff --git a/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt
index 9b9208a4..12cf3eb9 100644
--- a/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt
+++ b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/repository/DriveLinkSelectionRepository.kt
@@ -19,9 +19,18 @@ package me.proton.core.drive.drivelink.selection.domain.repository
import kotlinx.coroutines.flow.Flow
import me.proton.core.drive.drivelink.domain.entity.DriveLink
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.link.domain.entity.LinkId
import me.proton.core.drive.link.selection.domain.entity.SelectionId
interface DriveLinkSelectionRepository {
fun getSelectedDriveLinks(selectionId: SelectionId): Flow>
+
+ suspend fun selectAll(
+ parentId: FolderId,
+ selectionId: SelectionId?,
+ getDriveLinks: (fromIndex: Int, count: Int) -> Flow>,
+ selectLinks: suspend (SelectionId?, List) -> Result,
+ )
}
diff --git a/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt
new file mode 100644
index 00000000..3ee1719d
--- /dev/null
+++ b/drive/drivelink-selection/domain/src/main/kotlin/me/proton/core/drive/drivelink/selection/domain/usecase/SelectAll.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.drivelink.selection.domain.usecase
+
+import me.proton.core.drive.drivelink.domain.repository.DriveLinkRepository
+import me.proton.core.drive.drivelink.selection.domain.repository.DriveLinkSelectionRepository
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.link.selection.domain.entity.SelectionId
+import me.proton.core.drive.link.selection.domain.usecase.SelectLinks
+import javax.inject.Inject
+
+class SelectAll @Inject constructor(
+ private val selectLinks: SelectLinks,
+ private val driveLinkSelectionRepository: DriveLinkSelectionRepository,
+ private val driveLinkRepository: DriveLinkRepository,
+) {
+
+ suspend operator fun invoke(parentId: FolderId, selectionId: SelectionId?) = driveLinkSelectionRepository.selectAll(
+ parentId = parentId,
+ selectionId = selectionId,
+ getDriveLinks = { fromIndex, count ->
+ driveLinkRepository.getDriveLinks(parentId, fromIndex, count)
+ },
+ selectLinks = { id, linkIds ->
+ id?.let {
+ selectLinks(id, linkIds)
+ Result.success(id)
+ } ?: selectLinks(linkIds)
+ }
+ )
+}
diff --git a/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt
new file mode 100644
index 00000000..b4789f36
--- /dev/null
+++ b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorter.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.drivelink.sorting.domain.sorter
+
+import android.os.Build
+import me.proton.core.drive.drivelink.domain.entity.DriveLink
+import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
+import me.proton.core.drive.sorting.domain.entity.Direction
+import java.text.Collator
+import android.icu.text.Collator as IcuCollator
+import android.icu.text.RuleBasedCollator as IcuRuleBasedCollator
+
+object LocaleNameSorter : Sorter() {
+
+ private val comparator: Comparator =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ (IcuCollator.getInstance() as IcuRuleBasedCollator).apply {
+ numericCollation = true
+ }
+ } else {
+ Collator.getInstance()
+ }
+
+ override fun sort(driveLinks: List, direction: Direction): List =
+ driveLinks.sortedWith(
+ compareBy { driveLink -> if (driveLink is DriveLink.Folder) 0 else 1 }
+ .thenBy { driveLink -> if (driveLink.isNameEncrypted) 0 else 1 }
+ .thenComparator { a, b ->
+ when (direction) {
+ Direction.ASCENDING -> comparator.compare(a.name, b.name)
+ Direction.DESCENDING -> comparator.compare(b.name, a.name)
+ }
+ }
+ )
+}
diff --git a/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt
index 2c32920f..2dab3f6d 100644
--- a/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt
+++ b/drive/drivelink-sorting/domain/src/main/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Sorter.kt
@@ -28,7 +28,7 @@ sealed class Sorter {
companion object Factory {
operator fun get(by: By): Sorter = when (by) {
- By.NAME -> NameSorter
+ By.NAME -> LocaleNameSorter
By.LAST_MODIFIED -> LastModifiedSorter
By.SIZE -> SizeSorter
By.TYPE -> TypeSorter
diff --git a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt
new file mode 100644
index 00000000..2e9572dd
--- /dev/null
+++ b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/Files.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.drivelink.sorting.domain.sorter
+
+import io.mockk.every
+import io.mockk.mockk
+import me.proton.core.crypto.common.pgp.VerificationStatus
+import me.proton.core.drive.base.domain.entity.Bytes
+import me.proton.core.drive.base.domain.entity.CryptoProperty
+import me.proton.core.drive.base.domain.entity.TimestampS
+import me.proton.core.drive.drivelink.domain.entity.DriveLink
+
+fun file(name: String, type: String = "", lastModified: Long = 0L, size: Long = 0L) =
+ mockk()
+ .apply(name, type, lastModified, size)
+
+fun cryptedFile(name: String, type: String, lastModified: Long, size: Long) =
+ mockk()
+ .apply(name, type, lastModified, size)
+ .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) }
+
+fun folder(name: String, lastModified: Long, size: Long) = mockk()
+ .apply(name, "Folder", lastModified, size)
+
+fun cryptedFolder(name: String, lastModified: Long, size: Long) =
+ mockk()
+ .apply(name, "Folder", lastModified, size)
+ .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) }
+
+fun T.apply(
+ name: String,
+ type: String,
+ lastModifiedS: Long,
+ sizeB: Long
+) = apply {
+ every { cryptoName } returns CryptoProperty.Decrypted(name, VerificationStatus.Success)
+ every { this@apply.name } returns name
+ every { mimeType } returns type
+ every { lastModified } returns TimestampS(lastModifiedS)
+ every { size } returns Bytes(sizeB)
+}
\ No newline at end of file
diff --git a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt
new file mode 100644
index 00000000..9f57e8d5
--- /dev/null
+++ b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/LocaleNameSorterTest.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.drivelink.sorting.domain.sorter
+
+import me.proton.core.drive.sorting.domain.entity.By
+import me.proton.core.drive.sorting.domain.entity.Direction
+import org.junit.Assert.*
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+class LocaleNameSorterTest {
+
+ private val File1 = file("File1")
+ private val file2 = file("file2")
+ private val file22 = file("file22")
+ private val file3 = file("file3")
+ private val file4 = file("file4")
+ private val file45 = file("file45")
+ private val file5 = file("file5")
+
+ private val files = listOf(
+ file2,
+ File1,
+ file4,
+ file22,
+ file3,
+ file45,
+ file5,
+ )
+
+ @Test
+ @Config(sdk = [24])
+ fun `sort files by locale names ascending`() {
+
+ val sorted = Sorter.Factory[By.NAME].sort(files, Direction.ASCENDING)
+
+ assertEquals(
+ listOf(
+ File1,
+ file2,
+ file3,
+ file4,
+ file5,
+ file22,
+ file45,
+ ).map { it.name },
+ sorted.map { it.name },
+ )
+ }
+
+ @Test
+ @Config(sdk = [24])
+ fun `sort files by locale names descending`() {
+
+ val sorted = Sorter.Factory[By.NAME].sort(files, Direction.DESCENDING)
+
+ assertEquals(
+ listOf(
+ file45,
+ file22,
+ file5,
+ file4,
+ file3,
+ file2,
+ File1,
+ ).map { it.name },
+ sorted.map { it.name },
+ )
+ }
+
+ @Test
+ @Config(sdk = [23])
+ fun `sort files by names ascending`() {
+
+ val sorted = Sorter.Factory[By.NAME].sort(files, Direction.ASCENDING)
+
+ assertEquals(
+ listOf(
+ File1,
+ file2,
+ file22,
+ file3,
+ file4,
+ file45,
+ file5,
+ ).map { it.name },
+ sorted.map { it.name },
+ )
+ }
+
+ @Test
+ @Config(sdk = [23])
+ fun `sort files by names descending`() {
+
+ val sorted = Sorter.Factory[By.NAME].sort(files, Direction.DESCENDING)
+
+ assertEquals(
+ listOf(
+ file5,
+ file45,
+ file4,
+ file3,
+ file22,
+ file2,
+ File1,
+ ).map { it.name },
+ sorted.map { it.name },
+ )
+ }
+}
\ No newline at end of file
diff --git a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt
index e2b5f10b..cc3aa904 100644
--- a/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt
+++ b/drive/drivelink-sorting/domain/src/test/kotlin/me/proton/core/drive/drivelink/sorting/domain/sorter/SorterTest.kt
@@ -18,18 +18,14 @@
package me.proton.core.drive.drivelink.sorting.domain.sorter
-import io.mockk.every
-import io.mockk.mockk
import junit.framework.TestCase.assertEquals
-import me.proton.core.crypto.common.pgp.VerificationStatus
-import me.proton.core.drive.base.domain.entity.Bytes
-import me.proton.core.drive.base.domain.entity.TimestampS
-import me.proton.core.drive.base.domain.entity.CryptoProperty
-import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.sorting.domain.entity.By
import me.proton.core.drive.sorting.domain.entity.Direction
import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+@RunWith(RobolectricTestRunner::class)
class SorterTest {
private val drivelinks = listOf(
@@ -774,26 +770,4 @@ class SorterTest {
}
// endregion
// endregion
-
- private fun file(name: String, type: String, lastModified: Long, size: Long) = mockk()
- .apply(name, type, lastModified, size)
-
- private fun cryptedFile(name: String, type: String, lastModified: Long, size: Long) = mockk()
- .apply(name, type, lastModified, size)
- .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) }
-
- private fun folder(name: String, lastModified: Long, size: Long) = mockk()
- .apply(name, "Folder", lastModified, size)
-
- private fun cryptedFolder(name: String, lastModified: Long, size: Long) = mockk()
- .apply(name, "Folder", lastModified, size)
- .apply { every { cryptoName } returns CryptoProperty.Encrypted(name) }
-
- private fun T.apply(name: String, type: String, lastModifiedS: Long, sizeB: Long) = apply {
- every { cryptoName } returns CryptoProperty.Decrypted(name, VerificationStatus.Success)
- every { this@apply.name } returns name
- every { mimeType } returns type
- every { lastModified } returns TimestampS(lastModifiedS)
- every { size } returns Bytes(sizeB)
- }
}
diff --git a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt
index f3e9deda..4ba4d5d8 100644
--- a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt
+++ b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/db/dao/DriveLinkDao.kt
@@ -49,6 +49,18 @@ interface DriveLinkDao : LinkDao {
)
fun getLink(userId: UserId, shareId: String, linkId: String?): Flow>
+ @Query(
+ """
+ SELECT $DRIVE_LINK_SELECT FROM $DRIVE_LINK_ENTITY
+ WHERE
+ LinkEntity.user_id = :userId AND
+ LinkEntity.share_id = :shareId AND
+ LinkEntity.parent_id = :parentId AND
+ ${LinkTrashDao.NOT_TRASHED_CONDITION}
+ """
+ )
+ fun getLinks(userId: UserId, shareId: String, parentId: String): Flow>
+
@Query(
"""
SELECT $DRIVE_LINK_SELECT FROM $DRIVE_LINK_ENTITY
@@ -57,9 +69,20 @@ interface DriveLinkDao : LinkDao {
LinkEntity.share_id = :shareId AND
LinkEntity.parent_id = :parentId AND
${LinkTrashDao.NOT_TRASHED_CONDITION}
- """
+ LIMIT :limit OFFSET :offset
+ """
)
- fun getLinks(userId: UserId, shareId: String, parentId: String?): Flow>
+ fun getLinks(userId: UserId, shareId: String, parentId: String?, limit: Int, offset: Int): Flow>
+
+ @Query("""
+ SELECT COUNT(*) FROM (SELECT DISTINCT LinkEntity.id FROM $DRIVE_LINK_ENTITY
+ WHERE
+ LinkEntity.user_id = :userId AND
+ LinkEntity.share_id = :shareId AND
+ LinkEntity.parent_id = :parentId AND
+ ${LinkTrashDao.NOT_TRASHED_CONDITION})
+ """)
+ fun getLinksCountFlow(userId: UserId, shareId: String, parentId: String?): Flow
@Query(
"""
diff --git a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt
index 9a32c7ee..4c9e0f4a 100644
--- a/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt
+++ b/drive/drivelink/data/src/main/kotlin/me/proton/core/drive/drivelink/data/repository/DriveLinkRepositoryImpl.kt
@@ -48,6 +48,13 @@ class DriveLinkRepositoryImpl @Inject constructor(
.distinctUntilChanged()
.map { entities -> entities.toDriveLinks() }
+ override fun getDriveLinksCount(parentId: FolderId): Flow =
+ driveLinkDao.getLinksCountFlow(parentId.userId, parentId.shareId.id, parentId.id)
+
+ override fun getDriveLinks(parentId: FolderId, fromIndex: Int, count: Int): Flow> =
+ driveLinkDao.getLinks(parentId.userId, parentId.shareId.id, parentId.id, count, fromIndex)
+ .map { entities -> entities.toDriveLinks() }
+
override fun getDriveLinks(linkIds: List): Flow> =
linkIds
.groupBy({ linkId -> linkId.shareId }) { linkId -> linkId.id }
diff --git a/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt
index 866733d6..55208768 100644
--- a/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt
+++ b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/repository/DriveLinkRepository.kt
@@ -29,5 +29,9 @@ interface DriveLinkRepository {
fun getDriveLinks(parentId: FolderId): Flow>
+ fun getDriveLinks(parentId: FolderId, fromIndex: Int, count: Int): Flow>
+
+ fun getDriveLinksCount(parentId: FolderId): Flow
+
fun getDriveLinks(linkIds: List): Flow>
}
diff --git a/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt
new file mode 100644
index 00000000..ed69abc1
--- /dev/null
+++ b/drive/drivelink/domain/src/main/kotlin/me/proton/core/drive/drivelink/domain/usecase/GetDriveLinksCount.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.drivelink.domain.usecase
+
+import kotlinx.coroutines.flow.Flow
+import me.proton.core.drive.drivelink.domain.repository.DriveLinkRepository
+import me.proton.core.drive.link.domain.entity.FolderId
+import javax.inject.Inject
+
+class GetDriveLinksCount @Inject constructor(
+ private val repository: DriveLinkRepository,
+) {
+ operator fun invoke(parentId: FolderId): Flow =
+ repository.getDriveLinksCount(parentId)
+}
diff --git a/drive/event-manager/data/build.gradle.kts b/drive/event-manager/data/build.gradle.kts
index 52be4aef..4d3d753b 100644
--- a/drive/event-manager/data/build.gradle.kts
+++ b/drive/event-manager/data/build.gradle.kts
@@ -32,4 +32,5 @@ driveModule(
implementation(libs.core.dataRoom)
implementation(libs.core.presentation) // AppLifecycleProvider
implementation(libs.core.userSettings)
+ implementation(libs.core.user.data)
}
diff --git a/drive/file/base/domain/build.gradle.kts b/drive/file/base/domain/build.gradle.kts
index 8896b74a..9eae8d83 100644
--- a/drive/file/base/domain/build.gradle.kts
+++ b/drive/file/base/domain/build.gradle.kts
@@ -26,5 +26,4 @@ driveModule(
) {
api(project(":drive:link:domain"))
implementation(project(":drive:event-manager:base:domain"))
- implementation(project(":drive:base:data"))
}
diff --git a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt
index 33309a0a..e3df3355 100644
--- a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt
+++ b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/entity/XAttr.kt
@@ -17,8 +17,8 @@
*/
package me.proton.core.drive.file.base.domain.entity
-import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
@Serializable
data class XAttr(
@@ -35,6 +35,8 @@ data class XAttr(
val size: Long? = null,
@SerialName("BlockSizes")
val blockSizes: List? = null,
+ @SerialName("Digests")
+ val digests: Map? = null,
)
@Serializable
diff --git a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt
index f0477ccb..1abf75ad 100644
--- a/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt
+++ b/drive/file/base/domain/src/main/kotlin/me/proton/core/drive/file/base/domain/usecase/CreateXAttr.kt
@@ -39,12 +39,14 @@ class CreateXAttr @Inject constructor(
size: Bytes,
blockSizes: List,
mediaResolution: MediaResolution? = null,
+ digests: Map? = null,
) =
XAttr(
common = XAttr.Common(
modificationTime = dateTimeFormatter.formatToIso8601String(modificationTime),
size = size.value,
- blockSizes = blockSizes.map { blockSize -> blockSize.value }
+ blockSizes = blockSizes.map { blockSize -> blockSize.value },
+ digests = digests,
),
media = mediaResolution?.let {
XAttr.Media(
diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt
index 9f01c1bd..eaa976dd 100644
--- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt
+++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/Files.kt
@@ -17,11 +17,11 @@
*/
package me.proton.core.drive.files.presentation.component
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import me.proton.core.compose.flow.rememberFlowWithLifecycle
+import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing
import me.proton.core.drive.base.domain.entity.Percentage
import me.proton.core.drive.base.presentation.component.TopAppBar
import me.proton.core.drive.drivelink.domain.entity.DriveLink
@@ -125,6 +126,7 @@ private inline fun ListContent(
viewEvent: FilesViewEvent,
uploadFileLinkList: List,
lazyListState: LazyListState,
+ verticalArrangement : Arrangement.Vertical = Arrangement.Top,
crossinline content: LazyListScope.() -> Unit,
) {
val modifier = if (viewState.listContentState is ListContentState.Content) {
@@ -132,7 +134,11 @@ private inline fun ListContent(
} else {
Modifier
}
- LazyColumn(modifier = modifier.fillMaxSize(), state = lazyListState) {
+ LazyColumn(
+ modifier = modifier.fillMaxSize(),
+ state = lazyListState,
+ verticalArrangement = verticalArrangement,
+ ) {
itemsIndexed(uploadFileLinkList, key = { _, uploadFileLink -> uploadFileLink.id }) { index, uploadFileLink ->
UploadItem(viewState, viewEvent, index, uploadFileLink)
}
@@ -188,8 +194,14 @@ private fun LazyColumnItems.DisplayAsGrid(
BoxWithConstraints {
val itemsPerRow = floor(maxWidth / GridItemWidth).roundToInt().coerceAtLeast(1)
val lazyListState = this@DisplayAsGrid.rememberLazyListState()
- ListContent(viewState, viewEvent, uploadFileLinkList, lazyListState) {
- FilesGridContent(this@DisplayAsGrid, GridItemWidth, itemsPerRow) { driveLink: DriveLink ->
+ ListContent(
+ viewState = viewState,
+ viewEvent = viewEvent,
+ uploadFileLinkList = uploadFileLinkList,
+ lazyListState = lazyListState,
+ verticalArrangement = Arrangement.spacedBy(ExtraSmallSpacing)
+ ) {
+ FilesGridContent(this@DisplayAsGrid, itemsPerRow) { driveLink: DriveLink ->
val selected = selectedDriveLinks.contains(driveLink.id)
FilesGridItem(
link = driveLink,
@@ -200,7 +212,7 @@ private fun LazyColumnItems.DisplayAsGrid(
isClickEnabled = viewState.isClickEnabled,
isTextEnabled = viewState.isTextEnabled,
transferProgressFlow = remember(driveLink.downloadState) { getTransferProgress(driveLink) },
- modifier = Modifier.width(GridItemWidth),
+ modifier = Modifier.weight(1F),
isSelected = selected,
inMultiselect = selected || selectedDriveLinks.isNotEmpty(),
)
diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt
new file mode 100644
index 00000000..79840c3e
--- /dev/null
+++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/CircleSelection.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+
+package me.proton.core.drive.files.presentation.component.files
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import me.proton.core.compose.theme.ProtonDimens
+import me.proton.core.compose.theme.ProtonTheme
+import me.proton.core.drive.base.presentation.R as BasePresentation
+import me.proton.core.presentation.R as CorePresentation
+
+
+@Composable
+fun CircleSelection(isSelected: Boolean) {
+ if (isSelected) {
+ Image(
+ modifier = Modifier.size(ProtonDimens.DefaultIconSize),
+ painter = painterResource(id = BasePresentation.drawable.ic_checkmark_circle_filled),
+ contentDescription = null
+ )
+ } else {
+ Icon(
+ modifier = Modifier.size(ProtonDimens.DefaultIconSize),
+ painter = painterResource(id = CorePresentation.drawable.ic_proton_circle),
+ tint = ProtonTheme.colors.iconWeak,
+ contentDescription = null
+ )
+ }
+}
\ No newline at end of file
diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt
index c1e5a97c..a668d23e 100644
--- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt
+++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesGridItem.kt
@@ -18,6 +18,7 @@
package me.proton.core.drive.files.presentation.component.files
+import android.content.res.Configuration
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
@@ -29,14 +30,17 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.Checkbox
-import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -44,29 +48,29 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
+import me.proton.core.compose.flow.rememberFlowWithLifecycle
import me.proton.core.compose.theme.ProtonDimens.DefaultButtonMinHeight
import me.proton.core.compose.theme.ProtonDimens.DefaultIconSize
-import me.proton.core.compose.theme.ProtonTheme
-import me.proton.core.drive.drivelink.domain.entity.DriveLink
-import me.proton.core.drive.linkdownload.domain.entity.DownloadState
import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing
import me.proton.core.compose.theme.ProtonDimens.SmallIconSize
import me.proton.core.compose.theme.ProtonDimens.SmallSpacing
+import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.compose.theme.defaultSmall
import me.proton.core.drive.base.domain.entity.Percentage
-import me.proton.core.compose.flow.rememberFlowWithLifecycle
import me.proton.core.drive.base.presentation.component.EncryptedItem
import me.proton.core.drive.base.presentation.component.LinearProgressIndicator
-import me.proton.core.drive.base.presentation.component.protonColors
import me.proton.core.drive.base.presentation.component.text.TextWithMiddleEllipsis
+import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
+import me.proton.core.drive.linkdownload.domain.entity.DownloadState
import me.proton.core.drive.thumbnail.presentation.extension.thumbnailPainter
import me.proton.core.presentation.R as CorePresentation
@@ -102,6 +106,10 @@ fun FilesGridItem(
onLongClick(link)
},
)
+ .background(
+ color = if (isSelected) ProtonTheme.colors.backgroundSecondary else Color.Transparent,
+ shape = ProtonTheme.shapes.medium,
+ )
.padding(SmallSpacing),
) {
Column(
@@ -113,16 +121,11 @@ fun FilesGridItem(
Crossfade(inMultiselect) { inMultiselect ->
if (inMultiselect) {
Box(
- modifier = Modifier
- .size(IconSize)
- .padding(MoreButtonPadding),
+ modifier = Modifier.size(DefaultButtonMinHeight),
contentAlignment = Alignment.Center,
) {
- Checkbox(
- checked = isSelected,
- onCheckedChange = null,
- modifier = Modifier.scale(1.25f),
- colors = CheckboxDefaults.protonColors(),
+ CircleSelection(
+ isSelected = isSelected,
)
}
} else {
@@ -189,7 +192,9 @@ fun GridItemImage(
) {
Box(
modifier = modifier
- .size(ImageWidth, ImageHeight)
+ .defaultMinSize(ImageWidth, ImageHeight)
+ .fillMaxWidth()
+ .aspectRatio(ImageRatio)
.border(1.dp, ProtonTheme.colors.separatorNorm, ProtonTheme.shapes.medium)
.clip(ProtonTheme.shapes.medium)
) {
@@ -252,5 +257,96 @@ private fun GridItemMoreButton(
private val ImageHeight = 96.dp
private val ImageWidth = 158.dp
+private val ImageRatio = ImageWidth/ImageHeight
private val MoreButtonPadding = 12.dp
private const val MoreButtonAlpha = 0.7f
+
+@Preview(
+ name = "GridItem not downloaded, not favorite, not shared in light mode",
+ group = "light mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+)
+@Preview(
+ name = "GridItem not downloaded, not favorite, not shared in dark mode",
+ group = "dark mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+)
+@Suppress("unused")
+@Composable
+private fun PreviewGridItem() {
+ ProtonTheme {
+ Surface(modifier = Modifier.background(MaterialTheme.colors.background)) {
+ FilesGridItem(
+ link = PREVIEW_DRIVELINK,
+ onClick = {},
+ onLongClick = {},
+ onMoreOptionsClick = {},
+ isClickEnabled = { false },
+ isTextEnabled = { true },
+ )
+ }
+ }
+}
+@Preview(
+ name = "GridItem in multi select, selected in light mode",
+ group = "light mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+)
+@Preview(
+ name = "GridItem in multi select, selected in dark mode",
+ group = "dark mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+)
+@Suppress("unused")
+@Composable
+private fun PreviewSelectedGridItem() {
+ ProtonTheme {
+ Surface(modifier = Modifier.background(MaterialTheme.colors.background)) {
+ FilesGridItem(
+ link = PREVIEW_DRIVELINK,
+ onClick = {},
+ onLongClick = {},
+ onMoreOptionsClick = {},
+ isClickEnabled = { false },
+ isTextEnabled = { true },
+ isSelected = true,
+ inMultiselect = true,
+ )
+ }
+ }
+}
+@Preview(
+ name = "GridItem in multi select, unselected in light mode",
+ group = "light mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+)
+@Preview(
+ name = "GridItem in multi select, unselected in dark mode",
+ group = "dark mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+)
+@Suppress("unused")
+@Composable
+private fun PreviewUnselectedGridItem() {
+ ProtonTheme {
+ Surface(modifier = Modifier.background(MaterialTheme.colors.background)) {
+ FilesGridItem(
+ link = PREVIEW_DRIVELINK,
+ onClick = {},
+ onLongClick = {},
+ onMoreOptionsClick = {},
+ isClickEnabled = { false },
+ isTextEnabled = { true },
+ isSelected = false,
+ inMultiselect = true,
+ )
+ }
+ }
+}
+
diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt
index add7fa90..34d128bb 100644
--- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt
+++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesList.kt
@@ -24,11 +24,11 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
@@ -39,7 +39,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.Dp
import androidx.paging.compose.items
import me.proton.core.compose.component.DeferredCircularProgressIndicator
import me.proton.core.compose.component.ErrorPadding
@@ -47,6 +46,7 @@ import me.proton.core.compose.component.ProtonErrorMessage
import me.proton.core.compose.component.ProtonErrorMessageWithAction
import me.proton.core.compose.component.ProtonSecondaryButton
import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing
+import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing
import me.proton.core.compose.theme.ProtonDimens.MediumSpacing
import me.proton.core.compose.theme.ProtonDimens.SmallSpacing
import me.proton.core.compose.theme.ProtonTheme
@@ -199,22 +199,21 @@ fun LazyListScope.FilesListContent(
fun LazyListScope.FilesGridContent(
driveLinks: LazyColumnItems,
- itemSize: Dp,
itemsPerRow: Int,
- onItemsIndexed: @Composable (DriveLink) -> Unit,
+ onItemsIndexed: @Composable RowScope.(DriveLink) -> Unit,
) {
require(itemsPerRow > 0) { "itemsPerRow must be > 0, value passed $itemsPerRow" }
items(ceil(driveLinks.size.toFloat() / itemsPerRow).toInt()) { row ->
Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceEvenly,
+ modifier = Modifier.fillMaxWidth().padding(horizontal = ExtraSmallSpacing),
+ horizontalArrangement = Arrangement.spacedBy(ExtraSmallSpacing),
) {
repeat(itemsPerRow) { repeat ->
val driveLink = driveLinks[row * itemsPerRow + repeat]
if (driveLink != null) {
onItemsIndexed(driveLink)
} else {
- Spacer(modifier = Modifier.width(itemSize))
+ Spacer(modifier = Modifier.weight(1F))
}
}
}
diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt
index 3f980510..a6a3f809 100644
--- a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt
+++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/FilesListItem.kt
@@ -31,8 +31,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Checkbox
-import androidx.compose.material.CheckboxDefaults
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -44,7 +42,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
@@ -65,31 +62,20 @@ import me.proton.core.compose.theme.ProtonDimens.SmallSpacing
import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.compose.theme.captionWeak
import me.proton.core.compose.theme.default
-import me.proton.core.domain.entity.UserId
-import me.proton.core.drive.base.domain.entity.Attributes
-import me.proton.core.drive.base.domain.entity.Bytes
import me.proton.core.drive.base.domain.entity.Percentage
-import me.proton.core.drive.base.domain.entity.Permissions
-import me.proton.core.drive.base.domain.entity.TimestampS
import me.proton.core.drive.base.domain.extension.toPercentString
import me.proton.core.drive.base.presentation.component.EncryptedItem
import me.proton.core.drive.base.presentation.component.LinearProgressIndicator
-import me.proton.core.drive.base.presentation.component.protonColors
import me.proton.core.drive.base.presentation.component.text.TextWithMiddleEllipsis
import me.proton.core.drive.base.presentation.extension.currentLocale
import me.proton.core.drive.drivelink.domain.entity.DriveLink
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
-import me.proton.core.drive.link.domain.entity.FileId
import me.proton.core.drive.link.domain.entity.Folder
-import me.proton.core.drive.link.domain.entity.FolderId
-import me.proton.core.drive.link.domain.entity.Link
import me.proton.core.drive.link.domain.extension.isSharedUrlExpired
import me.proton.core.drive.link.presentation.extension.getSize
import me.proton.core.drive.link.presentation.extension.lastModifiedRelative
import me.proton.core.drive.linkdownload.domain.entity.DownloadState
-import me.proton.core.drive.share.domain.entity.ShareId
import me.proton.core.drive.thumbnail.presentation.extension.thumbnailPainter
-import me.proton.core.drive.volume.domain.entity.VolumeId
import me.proton.core.drive.base.presentation.R as BasePresentation
import me.proton.core.presentation.R as CorePresentation
@@ -118,7 +104,7 @@ fun FilesListItem(
Box(
modifier = modifier
.fillMaxWidth()
- .background(color = if (isSelected) ProtonTheme.colors.interactionWeakNorm else Color.Transparent)
+ .background(color = if (isSelected) ProtonTheme.colors.backgroundSecondary else Color.Transparent)
.combinedClickable(
enabled = onClick != null && isClickEnabled(link),
onClick = { onClick?.invoke(link) },
@@ -135,30 +121,14 @@ fun FilesListItem(
.padding(start = StartPadding, end = EndPadding)
.padding(vertical = VerticalPadding),
) {
- Crossfade(isSelected) { isSelected ->
- if (isSelected) {
- Box(
- modifier = Modifier
- .size(IconSize),
- contentAlignment = Alignment.Center,
- ) {
- Checkbox(
- checked = true,
- onCheckedChange = null,
- modifier = Modifier.scale(1.75f),
- colors = CheckboxDefaults.protonColors(),
- )
- }
- } else {
- Image(
- modifier = Modifier
- .size(IconSize)
- .clip(RoundedCornerShape(DefaultCornerRadius)),
- painter = link.thumbnailPainter().painter,
- contentDescription = null,
- )
- }
- }
+
+ Image(
+ modifier = Modifier
+ .size(IconSize)
+ .clip(RoundedCornerShape(DefaultCornerRadius)),
+ painter = link.thumbnailPainter().painter,
+ contentDescription = null,
+ )
Details(
modifier = Modifier
.weight(1f)
@@ -186,6 +156,15 @@ fun FilesListItem(
)
}
}
+ } else {
+ Crossfade(isSelected) { isSelected ->
+ Box(
+ modifier = Modifier.size(DefaultButtonMinHeight),
+ contentAlignment = Alignment.Center,
+ ) {
+ CircleSelection(isSelected)
+ }
+ }
}
}
}
@@ -409,6 +388,67 @@ fun PreviewListItem() {
}
}
}
+@Preview(
+ name = "ListItem in multiselect, selected, in light mode",
+ group = "light mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+)
+@Preview(
+ name = "ListItem in multiselect, selected, in dark mode",
+ group = "dark mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+)
+@Suppress("unused")
+@Composable
+fun PreviewSelectedListItem() {
+ ProtonTheme {
+ Surface(modifier = Modifier.background(MaterialTheme.colors.background)) {
+ FilesListItem(
+ link = PREVIEW_DRIVELINK,
+ onClick = {},
+ onLongClick = {},
+ onMoreOptionsClick = {},
+ isClickEnabled = { false },
+ isTextEnabled = { true },
+ isSelected = true,
+ inMultiselect = true,
+ )
+ }
+ }
+}
+
+@Preview(
+ name = "ListItem in multiselect, unselected, in light mode",
+ group = "light mode",
+ uiMode = Configuration.UI_MODE_NIGHT_NO,
+ showBackground = true,
+)
+@Preview(
+ name = "ListItem in multiselect, unselected, in dark mode",
+ group = "dark mode",
+ uiMode = Configuration.UI_MODE_NIGHT_YES,
+ showBackground = true,
+)
+@Suppress("unused")
+@Composable
+fun PreviewUnselectedListItem() {
+ ProtonTheme {
+ Surface(modifier = Modifier.background(MaterialTheme.colors.background)) {
+ FilesListItem(
+ link = PREVIEW_DRIVELINK,
+ onClick = {},
+ onLongClick = {},
+ onMoreOptionsClick = {},
+ isClickEnabled = { false },
+ isTextEnabled = { true },
+ isSelected = false,
+ inMultiselect = true,
+ )
+ }
+ }
+}
@Preview(
name = "ListItem downloaded but not favorite, not shared in light mode",
@@ -553,49 +593,6 @@ val Height = 68.dp
val TitleStartPadding = 20.dp
val ExtraSmallIconSize = 12.dp
-private val PREVIEW_LINK = Link.File(
- id = FileId(ShareId(UserId("USER_ID"), "SHARE_ID"), "FILE_ID"),
- parentId = FolderId(ShareId(UserId("USER_ID"), "SHARE_ID"), "PARENT_ID"),
- name = "revision_id",
- size = Bytes(0L),
- lastModified = TimestampS(0),
- mimeType = "text/plain",
- isShared = false,
- key = "",
- passphrase = "",
- passphraseSignature = "",
- numberOfAccesses = 0L,
- uploadedBy = "He-Who-Must-Not-Be-Named",
- isFavorite = false,
- hasThumbnail = false,
- activeRevisionId = "",
- xAttr = null,
- contentKeyPacket = "",
- contentKeyPacketSignature = "",
- attributes = Attributes(0),
- permissions = Permissions(0),
- state = Link.State.ACTIVE,
- nameSignatureEmail = "",
- hash = "",
- expirationTime = null,
- nodeKey = "",
- nodePassphrase = "",
- nodePassphraseSignature = "",
- signatureAddress = "",
- creationTime = TimestampS(0),
- trashedTime = null,
- shareUrlExpirationTime = null,
- shareUrlId = null,
-)
-private val PREVIEW_DRIVELINK = DriveLink.File(
- link = PREVIEW_LINK,
- volumeId = VolumeId("VOLUME_ID"),
- isMarkedAsOffline = false,
- isAnyAncestorMarkedAsOffline = false,
- downloadState = null,
- trashState = null,
-)
-
object FilesListItemComponentTestTag {
const val item = "file list item"
const val folder = "folder item"
diff --git a/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt
new file mode 100644
index 00000000..c28f165e
--- /dev/null
+++ b/drive/files-list/src/main/kotlin/me/proton/core/drive/files/presentation/component/files/Previews.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.files.presentation.component.files
+
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.domain.entity.Attributes
+import me.proton.core.drive.base.domain.entity.Bytes
+import me.proton.core.drive.base.domain.entity.Permissions
+import me.proton.core.drive.base.domain.entity.TimestampS
+import me.proton.core.drive.drivelink.domain.entity.DriveLink
+import me.proton.core.drive.link.domain.entity.FileId
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.link.domain.entity.Link
+import me.proton.core.drive.share.domain.entity.ShareId
+import me.proton.core.drive.volume.domain.entity.VolumeId
+
+internal val PREVIEW_LINK = Link.File(
+ id = FileId(ShareId(UserId("USER_ID"), "SHARE_ID"), "FILE_ID"),
+ parentId = FolderId(ShareId(UserId("USER_ID"), "SHARE_ID"), "PARENT_ID"),
+ name = "revision_id",
+ size = Bytes(0L),
+ lastModified = TimestampS(0),
+ mimeType = "text/plain",
+ isShared = false,
+ key = "",
+ passphrase = "",
+ passphraseSignature = "",
+ numberOfAccesses = 0L,
+ uploadedBy = "He-Who-Must-Not-Be-Named",
+ isFavorite = false,
+ hasThumbnail = false,
+ activeRevisionId = "",
+ xAttr = null,
+ contentKeyPacket = "",
+ contentKeyPacketSignature = "",
+ attributes = Attributes(0),
+ permissions = Permissions(0),
+ state = Link.State.ACTIVE,
+ nameSignatureEmail = "",
+ hash = "",
+ expirationTime = null,
+ nodeKey = "",
+ nodePassphrase = "",
+ nodePassphraseSignature = "",
+ signatureAddress = "",
+ creationTime = TimestampS(0),
+ trashedTime = null,
+ shareUrlExpirationTime = null,
+ shareUrlId = null,
+)
+internal val PREVIEW_DRIVELINK = DriveLink.File(
+ link = PREVIEW_LINK,
+ volumeId = VolumeId("VOLUME_ID"),
+ isMarkedAsOffline = false,
+ isAnyAncestorMarkedAsOffline = false,
+ downloadState = null,
+ trashState = null,
+)
\ No newline at end of file
diff --git a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt
index 66d1ee02..e38c3a79 100644
--- a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt
+++ b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolder.kt
@@ -27,11 +27,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.android.awaitFrame
import kotlinx.coroutines.delay
@@ -77,7 +81,7 @@ fun CreateFolder(
fun CreateFolder(
@StringRes titleResId: Int,
folderName: String,
- selection: IntRange,
+ selection: IntRange?,
showProgress: Boolean,
modifier: Modifier = Modifier,
inputError: String? = null,
@@ -116,7 +120,7 @@ fun CreateFolder(
@Composable
fun CreateFolderContent(
folderName: String,
- selection: IntRange,
+ selection: IntRange?,
modifier: Modifier = Modifier,
focusRequester: FocusRequester = remember { FocusRequester() },
inputError: String? = null,
@@ -127,12 +131,38 @@ fun CreateFolderContent(
.height(intrinsicSize = IntrinsicSize.Max)
) {
Spacer(modifier = Modifier.size(DefaultSpacing))
+ var state by remember {
+ mutableStateOf(
+ TextFieldValue(
+ text = folderName,
+ selection = if (selection == null) {
+ TextRange.Zero
+ } else {
+ TextRange(selection.first, selection.last)
+ }
+ )
+ )
+ }
+ LaunchedEffect(folderName, selection) {
+ state = state.copy(
+ text = folderName,
+ selection = if (selection != null) {
+ TextRange(selection.first, selection.last)
+ } else {
+ state.selection
+ }
+ )
+ }
OutlinedTextFieldWithError(
- text = folderName,
- selection = selection,
+ textFieldValue = state,
errorText = inputError,
focusRequester = focusRequester,
- onValueChanged = onValueChanged,
+ onValueChanged = { textFieldValue ->
+ if (state != textFieldValue) {
+ state = textFieldValue
+ onValueChanged(textFieldValue.text)
+ }
+ },
modifier = Modifier
.testTag(CreateFolderComponentTestTag.folderNameTextField),
)
diff --git a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt
index 16b52a96..2f0d7118 100644
--- a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt
+++ b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewModel.kt
@@ -71,7 +71,7 @@ class CreateFolderViewModel @Inject constructor(
) { name, error, inProgress ->
initialViewState.copy(
name = name,
- selection = if (error != null) name.selectAll else name.endPosition,
+ selection = if (error != null) name.selectAll else null,
error = error,
inProgress = inProgress
)
@@ -133,9 +133,6 @@ class CreateFolderViewModel @Inject constructor(
private val String.selectAll
get() = IntRange(0, length)
- private val String.endPosition
- get() = IntRange(length, length)
-
companion object {
const val KEY_SHARE_ID = "shareId"
const val KEY_PARENT_ID = "parentId"
diff --git a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt
index ff6d8fd2..2731b5a1 100644
--- a/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt
+++ b/drive/folder-create/presentation/src/main/kotlin/me/proton/core/drive/folder/create/presentation/CreateFolderViewState.kt
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable
data class CreateFolderViewState(
@StringRes val titleResId: Int,
val name: String,
- val selection: IntRange = IntRange(name.length, name.length),
+ val selection: IntRange? = null,
val error: String? = null,
val inProgress: Boolean = false
)
diff --git a/drive/link-trash/data-test/build.gradle.kts b/drive/link-trash/data-test/build.gradle.kts
new file mode 100644
index 00000000..3c52a90a
--- /dev/null
+++ b/drive/link-trash/data-test/build.gradle.kts
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+driveModule(
+ hilt = true,
+) {
+ api(project(":drive:link-trash:domain"))
+ implementation(project(":drive:link:data-test"))
+}
+
+configureJacoco()
diff --git a/drive/link-trash/data-test/src/main/AndroidManifest.xml b/drive/link-trash/data-test/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..0b69d23f
--- /dev/null
+++ b/drive/link-trash/data-test/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt
new file mode 100644
index 00000000..179c0d35
--- /dev/null
+++ b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/di/TestLinkTrashBindModule.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.linktrash.data.test.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import me.proton.core.drive.linktrash.data.test.repository.StubbedLinkTrashRepository
+import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository
+import javax.inject.Singleton
+
+@Suppress("unused")
+@ExperimentalCoroutinesApi
+@Module
+@InstallIn(SingletonComponent::class)
+interface TestLinkTrashBindModule {
+ @Binds
+ @Singleton
+ fun bindLinkTrashRepository(repository: StubbedLinkTrashRepository): LinkTrashRepository
+}
diff --git a/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt
new file mode 100644
index 00000000..46a06e87
--- /dev/null
+++ b/drive/link-trash/data-test/src/main/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepository.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.linktrash.data.test.repository
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.transform
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.arch.ResponseSource
+import me.proton.core.drive.link.data.test.NullableFile
+import me.proton.core.drive.link.data.test.NullableFolder
+import me.proton.core.drive.link.domain.entity.BaseLink
+import me.proton.core.drive.link.domain.entity.FileId
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.link.domain.entity.Link
+import me.proton.core.drive.link.domain.entity.LinkId
+import me.proton.core.drive.linktrash.domain.entity.TrashState
+import me.proton.core.drive.linktrash.domain.repository.LinkTrashRepository
+import me.proton.core.drive.share.domain.entity.ShareId
+import javax.inject.Inject
+
+class StubbedLinkTrashRepository @Inject constructor() : LinkTrashRepository {
+
+ private val workIds = MutableStateFlow(emptyMap>())
+ private val stateFlow = MutableStateFlow(emptyMap, TrashState>())
+ private var trashContent = emptyList()
+
+ val state: Map, TrashState>
+ get() = stateFlow.value
+
+ override suspend fun insertOrUpdateTrashState(linkIds: List, trashState: TrashState) {
+ stateFlow.value = stateFlow.value + (linkIds to trashState)
+ }
+
+ override suspend fun removeTrashState(linkIds: List) {
+ stateFlow.value = stateFlow.value.filterKeys { it != linkIds }
+ }
+
+ override suspend fun markTrashedLinkAsDeleted(shareId: ShareId) {
+ stateFlow.value = stateFlow.value.map { (linkIds, trashState) ->
+ if (linkIds.any { linkId -> linkId.shareId == shareId }) {
+ linkIds to TrashState.DELETED
+ } else {
+ linkIds to trashState
+ }
+ }.toMap()
+ }
+
+ override fun hasTrashContent(shareId: ShareId): Flow {
+ return stateFlow.transform { state ->
+ emit(
+ state.filterKeys { it.any { linkId -> linkId.shareId == shareId } }
+ .filterValues { it in listOf(TrashState.TRASHING, TrashState.DELETED) }
+ .isNotEmpty()
+ )
+ }
+ }
+
+ override suspend fun hasWorkWithId(workId: String): Boolean {
+ return workIds.value.containsKey(workId)
+ }
+
+ override suspend fun insertOrIgnoreWorkId(linkIds: List, workId: String) {
+ workIds.value = workIds.value + (workId to linkIds)
+ }
+
+ override suspend fun insertWork(linkIds: List, retries: Int): DataResult {
+ val workId = "work-id-${linkIds.first().shareId.id}"
+ workIds.value = workIds.value + (workId to linkIds)
+ return DataResult.Success(ResponseSource.Local, workId)
+ }
+
+ override suspend fun getLinksAndRemoveWorkFromCache(workId: String): List {
+ val work = workIds.value.filterKeys { id -> id == workId }
+ workIds.value = workIds.value.filterNot { (id, _) -> id == workId }
+ return work.values.map { linkIds ->
+ linkIds.map { linkId ->
+ val parentId = FolderId(linkId.shareId, "folder-id")
+ when (linkId) {
+ is FolderId -> NullableFolder(id = linkId, parentId = parentId)
+ is FileId -> NullableFile(id = linkId, parentId = parentId)
+ } as Link
+ }
+ }.flatten()
+ }
+
+ override suspend fun markTrashContentAsFetched(shareId: ShareId) {
+ trashContent = trashContent + shareId
+ }
+
+ override suspend fun shouldInitiallyFetchTrashContent(shareId: ShareId): Boolean {
+ return trashContent.contains(shareId).not()
+ }
+
+ override suspend fun isTrashed(linkId: LinkId): Boolean {
+ return stateFlow.value
+ .filterKeys { linkIds -> linkId in linkIds }
+ .filterValues { trashState -> trashState.isTrashed() }
+ .isNotEmpty()
+ }
+
+ override suspend fun isAnyTrashed(linkIds: Set): Boolean {
+ return linkIds.fold(false) { acc, linkId ->
+ acc || isTrashed(linkId)
+ }
+ }
+
+ private fun TrashState.isTrashed(): Boolean {
+ return this !in listOf(TrashState.TRASHING, TrashState.DELETED)
+ }
+}
+
+val LinkTrashRepository.state
+ get() = (this as StubbedLinkTrashRepository).state
+
+fun LinkTrashRepository.stateForLinks(vararg links: BaseLink): TrashState =
+ (this as StubbedLinkTrashRepository).state.getValue(links.map { it.id })
\ No newline at end of file
diff --git a/drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt b/drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt
new file mode 100644
index 00000000..8b36767b
--- /dev/null
+++ b/drive/link-trash/data-test/src/test/kotlin/me/proton/core/drive/linktrash/data/test/repository/StubbedLinkTrashRepositoryTest.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.linktrash.data.test.repository
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.link.domain.entity.FileId
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.linktrash.domain.entity.TrashState
+import me.proton.core.drive.share.domain.entity.ShareId
+import org.junit.Assert.*
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class StubbedLinkTrashRepositoryTest {
+
+ private val repository = StubbedLinkTrashRepository()
+
+
+ private val userId = UserId("user-id")
+ private val shareId = ShareId(userId, "share-id")
+ private val folderId = FolderId(shareId, "folder-id")
+ private val otherShareId = ShareId(userId, "other-share-id")
+ private val otherFolderId = FolderId(otherShareId, "other-folder-id")
+
+ @Test
+ fun insertOrUpdateTrashState() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING)
+
+ assertEquals(TrashState.TRASHING, repository.state[listOf(folderId)])
+ }
+
+ @Test
+ fun removeTrashState() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING)
+ repository.insertOrUpdateTrashState(listOf(otherFolderId), TrashState.TRASHING)
+
+ repository.removeTrashState(listOf(folderId))
+
+ assertEquals(mapOf(listOf(otherFolderId) to TrashState.TRASHING), repository.state)
+ }
+
+ @Test
+ fun markTrashedLinkAsDeleted() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING)
+ repository.insertOrUpdateTrashState(listOf(otherFolderId), TrashState.TRASHING)
+
+ repository.markTrashedLinkAsDeleted(shareId)
+
+ assertEquals(
+ mapOf(
+ listOf(folderId) to TrashState.DELETED,
+ listOf(otherFolderId) to TrashState.TRASHING,
+ ), repository.state
+ )
+ }
+
+ @Test
+ fun hasTrashContent() = runTest {
+ val hasTrashContent = repository.hasTrashContent(shareId)
+ assertFalse(hasTrashContent.first())
+
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING)
+
+ assertTrue(hasTrashContent.first())
+ }
+
+ @Test
+ fun hasWorkWithId() = runTest {
+ assertFalse(repository.hasWorkWithId("work-id-share-id"))
+
+ repository.insertWork(listOf(folderId))
+
+ assertTrue(repository.hasWorkWithId("work-id-share-id"))
+ }
+
+ @Test
+ fun insertOrIgnoreWorkId() = runTest {
+ val workId = "work-id"
+
+ repository.insertOrIgnoreWorkId(listOf(folderId), workId)
+
+ assertTrue(repository.hasWorkWithId(workId))
+ }
+
+ @Test
+ fun insertWork() = runTest {
+ repository.insertWork(listOf(folderId))
+
+ assertTrue(repository.hasWorkWithId("work-id-share-id"))
+ }
+
+ @Test
+ fun `Given folder id When getLinksAndRemoveWorkFromCache Then returns a folder`() = runTest {
+ val workId = "work-id"
+
+ repository.insertOrIgnoreWorkId(listOf(folderId), workId)
+
+ val links = repository.getLinksAndRemoveWorkFromCache(workId)
+
+ assertEquals(folderId, links.first().id)
+ assertFalse(repository.hasWorkWithId(workId))
+ }
+
+ @Test
+ fun `Given file id When getLinksAndRemoveWorkFromCache Then returns a file`() = runTest {
+ val workId = "work-id"
+ val fileId = FileId(shareId, "file-id")
+
+ repository.insertOrIgnoreWorkId(listOf(fileId), workId)
+
+ val links = repository.getLinksAndRemoveWorkFromCache(workId)
+
+ assertEquals(fileId, links.first().id)
+ assertFalse(repository.hasWorkWithId(workId))
+ }
+
+ @Test
+ fun shouldInitiallyFetchTrashContent() = runTest {
+ assertTrue(repository.shouldInitiallyFetchTrashContent(shareId))
+
+ repository.markTrashContentAsFetched(shareId)
+
+ assertFalse(repository.shouldInitiallyFetchTrashContent(shareId))
+ }
+
+ @Test
+ fun `Given empty When isTrashed Then returns false`() = runTest {
+ assertFalse(repository.isTrashed(folderId))
+ }
+
+ @Test
+ fun `Given trashed folder When isTrashed Then returns true`() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHED)
+
+ assertTrue(repository.isTrashed(folderId))
+ }
+
+ @Test
+ fun `Given trashing folder When isTrashed Then returns false`() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING)
+
+ assertFalse(repository.isTrashed(folderId))
+ }
+
+ @Test
+ fun `Given empty When isAnyTrashed Then returns false`() = runTest {
+ assertFalse(repository.isAnyTrashed(setOf(folderId)))
+ }
+
+ @Test
+ fun `Given trashed folder When isAnyTrashed Then returns true`() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHED)
+
+ assertTrue(repository.isAnyTrashed(setOf(folderId)))
+ }
+
+ @Test
+ fun `Given trashing folder When isAnyTrashed Then returns false`() = runTest {
+ repository.insertOrUpdateTrashState(listOf(folderId), TrashState.TRASHING)
+
+ assertFalse(repository.isAnyTrashed(setOf(folderId)))
+ }
+}
\ No newline at end of file
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt
index bfe42840..b6632a80 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/dao/LinkUploadDao.kt
@@ -131,6 +131,12 @@ abstract class LinkUploadDao : BaseDao() {
WHERE id = :id
""")
abstract fun updateMediaResolution(id: Long, mediaResolutionWidth: Long, mediaResolutionHeight: Long)
+ @Query("""
+ UPDATE LinkUploadEntity SET
+ digests = :digests
+ WHERE id = :id
+ """)
+ abstract fun updateDigests(id: Long, digests: String)
open fun getDistinctFlow(id: Long) = getFlow(id).distinctUntilChanged()
}
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt
index c5ee3479..4275d765 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/db/entity/LinkUploadEntity.kt
@@ -27,6 +27,7 @@ import me.proton.core.domain.entity.UserId
import me.proton.core.drive.base.data.db.Column
import me.proton.core.drive.base.data.db.Column.CONTENT_KEY_PACKET
import me.proton.core.drive.base.data.db.Column.CONTENT_KEY_PACKET_SIGNATURE
+import me.proton.core.drive.base.data.db.Column.DIGESTS
import me.proton.core.drive.base.data.db.Column.ID
import me.proton.core.drive.base.data.db.Column.LAST_MODIFIED
import me.proton.core.drive.base.data.db.Column.LINK_ID
@@ -113,4 +114,6 @@ data class LinkUploadEntity(
val mediaResolutionWidth: Long? = null,
@ColumnInfo(name = MEDIA_RESOLUTION_HEIGHT, defaultValue = "NULL")
val mediaResolutionHeight: Long? = null,
+ @ColumnInfo(name = DIGESTS, defaultValue = "NULL")
+ val digests: String? = null,
)
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt
index 565a303a..ff0c0400 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/extension/LinkUploadEntity.kt
@@ -17,14 +17,18 @@
*/
package me.proton.core.drive.linkupload.data.extension
+import kotlinx.serialization.SerializationException
+import me.proton.core.data.room.BuildConfig
import me.proton.core.drive.base.domain.entity.MediaResolution
import me.proton.core.drive.base.domain.entity.TimestampMs
import me.proton.core.drive.base.domain.extension.bytes
import me.proton.core.drive.link.domain.entity.FolderId
import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity
+import me.proton.core.drive.linkupload.domain.entity.UploadDigests
import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
import me.proton.core.drive.share.domain.entity.ShareId
import me.proton.core.drive.volume.domain.entity.VolumeId
+import me.proton.core.util.kotlin.deserialize
import me.proton.core.util.kotlin.takeIfNotEmpty
fun LinkUploadEntity.toUploadFileLink() =
@@ -54,5 +58,15 @@ fun LinkUploadEntity.toUploadFileLink() =
width = requireNotNull(mediaResolutionWidth),
height = requireNotNull(mediaResolutionHeight),
)
- }
+ },
+ digests = digests?.let { json ->
+ try {
+ UploadDigests(json.deserialize())
+ }catch (e: SerializationException){
+ if (BuildConfig.DEBUG) {
+ throw e
+ }
+ UploadDigests()
+ }
+ } ?: UploadDigests()
)
diff --git a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt
index a4d8a859..8e9e04e9 100644
--- a/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt
+++ b/drive/link-upload/data/src/main/kotlin/me/proton/core/drive/linkupload/data/repository/LinkUploadRepositoryImpl.kt
@@ -37,10 +37,12 @@ import me.proton.core.drive.linkupload.data.extension.toUploadBulkEntity
import me.proton.core.drive.linkupload.data.extension.toUploadFileLink
import me.proton.core.drive.linkupload.domain.entity.UploadBlock
import me.proton.core.drive.linkupload.domain.entity.UploadBulk
+import me.proton.core.drive.linkupload.domain.entity.UploadDigests
import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
import me.proton.core.drive.linkupload.domain.entity.UploadState
import me.proton.core.drive.linkupload.domain.factory.UploadBlockFactory
import me.proton.core.drive.linkupload.domain.repository.LinkUploadRepository
+import me.proton.core.util.kotlin.serialize
import javax.inject.Inject
class LinkUploadRepositoryImpl @Inject constructor(
@@ -143,6 +145,11 @@ class LinkUploadRepositoryImpl @Inject constructor(
mediaResolutionWidth = mediaResolution.width,
mediaResolutionHeight = mediaResolution.height,
)
+ override suspend fun updateUploadFileLinkDigests(uploadFileLinkId: Long, digests: UploadDigests) =
+ db.linkUploadDao.updateDigests(
+ id = uploadFileLinkId,
+ digests = digests.values.serialize()
+ )
override suspend fun removeUploadFileLink(uploadFileLinkId: Long) =
db.linkUploadDao.delete(uploadFileLinkId)
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt
new file mode 100644
index 00000000..fa3c18fd
--- /dev/null
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadDigests.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.linkupload.domain.entity
+
+data class UploadDigests(val values: Map = emptyMap())
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt
index abff3069..93d0baa1 100644
--- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/entity/UploadFileLink.kt
@@ -47,4 +47,5 @@ data class UploadFileLink(
val uriString: String? = null,
val shouldDeleteSourceUri: Boolean = false,
val mediaResolution: MediaResolution? = null,
+ val digests : UploadDigests = UploadDigests()
)
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt
index 660afc12..7bc5a935 100644
--- a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/repository/LinkUploadRepository.kt
@@ -26,6 +26,7 @@ import me.proton.core.drive.link.domain.entity.FileId
import me.proton.core.drive.link.domain.entity.FolderId
import me.proton.core.drive.linkupload.domain.entity.UploadBlock
import me.proton.core.drive.linkupload.domain.entity.UploadBulk
+import me.proton.core.drive.linkupload.domain.entity.UploadDigests
import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
import me.proton.core.drive.linkupload.domain.entity.UploadState
@@ -75,6 +76,8 @@ interface LinkUploadRepository {
suspend fun updateUploadFileLinkMediaResolution(uploadFileLinkId: Long, mediaResolution: MediaResolution)
+ suspend fun updateUploadFileLinkDigests(uploadFileLinkId: Long, digests: UploadDigests)
+
suspend fun removeUploadFileLink(uploadFileLinkId: Long)
suspend fun insertUploadBlocks(uploadFileLink: UploadFileLink, uploadBlocks: List)
diff --git a/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt
new file mode 100644
index 00000000..f9a62db2
--- /dev/null
+++ b/drive/link-upload/domain/src/main/kotlin/me/proton/core/drive/linkupload/domain/usecase/UpdateDigests.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2022-2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+package me.proton.core.drive.linkupload.domain.usecase
+
+import me.proton.core.drive.linkupload.domain.entity.UploadDigests
+import me.proton.core.drive.linkupload.domain.entity.UploadFileLink
+import me.proton.core.drive.linkupload.domain.repository.LinkUploadRepository
+import javax.inject.Inject
+
+class UpdateDigests @Inject constructor(
+ private val linkUploadRepository: LinkUploadRepository,
+ private val getUploadFileLinkAfterOperation: GetUploadFileLinkAfterOperation,
+) {
+ suspend operator fun invoke(
+ uploadFileLinkId: Long,
+ digests: UploadDigests,
+ ): Result = getUploadFileLinkAfterOperation(uploadFileLinkId) {
+ linkUploadRepository.updateUploadFileLinkDigests(uploadFileLinkId, digests)
+ }
+}
diff --git a/drive/link/data-test/build.gradle.kts b/drive/link/data-test/build.gradle.kts
new file mode 100644
index 00000000..befc3d36
--- /dev/null
+++ b/drive/link/data-test/build.gradle.kts
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+plugins {
+ id("com.android.library")
+}
+
+driveModule() {
+ api(project(":drive:link:domain"))
+}
diff --git a/drive/link/data-test/src/main/AndroidManifest.xml b/drive/link/data-test/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..954a3398
--- /dev/null
+++ b/drive/link/data-test/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt b/drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt
new file mode 100644
index 00000000..b595e42e
--- /dev/null
+++ b/drive/link/data-test/src/main/kotlin/me/proton/core/drive/link/data/test/NullableBaseLink.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+@file:Suppress("FunctionName")
+/*
+ * Copyright (c) 2023 Proton Technologies AG
+ * This file is part of Proton Technologies AG and ProtonCore.
+ *
+ * ProtonCore is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ProtonCore 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 ProtonCore. If not, see .
+ */
+package me.proton.core.drive.link.data.test
+
+import me.proton.core.drive.base.domain.entity.Attributes
+import me.proton.core.drive.base.domain.entity.Bytes
+import me.proton.core.drive.base.domain.entity.Permissions
+import me.proton.core.drive.base.domain.entity.TimestampS
+import me.proton.core.drive.link.domain.entity.File
+import me.proton.core.drive.link.domain.entity.FileId
+import me.proton.core.drive.link.domain.entity.Folder
+import me.proton.core.drive.link.domain.entity.FolderId
+import me.proton.core.drive.link.domain.entity.Link
+import me.proton.core.drive.shareurl.base.domain.entity.ShareUrlId
+
+fun NullableFile(parentId: FolderId, filename: String = "test-file"): File {
+ return NullableFile(id = FileId(parentId.shareId, filename), parentId = parentId)
+}
+
+fun NullableFile(
+ id: FileId,
+ parentId: FolderId,
+ name: String = "",
+ size: Bytes = Bytes(0L),
+ lastModified: TimestampS = TimestampS(0L),
+ mimeType: String = "",
+ isShared: Boolean = false,
+ key: String = "",
+ passphrase: String = "",
+ passphraseSignature: String = "",
+ numberOfAccesses: Long = 0,
+ shareUrlExpirationTime: TimestampS? = null,
+ uploadedBy: String = "",
+ isFavorite: Boolean = false,
+ attributes: Attributes = Attributes(0L),
+ permissions: Permissions = Permissions(0L),
+ state: Link.State = Link.State.ACTIVE,
+ nameSignatureEmail: String? = null,
+ hash: String = "",
+ expirationTime: TimestampS? = null,
+ nodeKey: String = "",
+ nodePassphrase: String = "",
+ nodePassphraseSignature: String = "",
+ signatureAddress: String = "",
+ creationTime: TimestampS = TimestampS(0L),
+ trashedTime: TimestampS? = null,
+ hasThumbnail: Boolean = false,
+ activeRevisionId: String = "",
+ xAttr: String? = null,
+ shareUrlId: ShareUrlId? = null,
+ contentKeyPacket: String = "",
+ contentKeyPacketSignature: String? = null,
+): File {
+ return Link.File(
+ id = id,
+ parentId = parentId,
+ name = name,
+ size = size,
+ lastModified = lastModified,
+ mimeType = mimeType,
+ isShared = isShared,
+ key = key,
+ passphrase = passphrase,
+ passphraseSignature = passphraseSignature,
+ numberOfAccesses = numberOfAccesses,
+ shareUrlExpirationTime = shareUrlExpirationTime,
+ uploadedBy = uploadedBy,
+ isFavorite = isFavorite,
+ attributes = attributes,
+ permissions = permissions,
+ state = state,
+ nameSignatureEmail = nameSignatureEmail,
+ hash = hash,
+ expirationTime = expirationTime,
+ nodeKey = nodeKey,
+ nodePassphrase = nodePassphrase,
+ nodePassphraseSignature = nodePassphraseSignature,
+ signatureAddress = signatureAddress,
+ creationTime = creationTime,
+ trashedTime = trashedTime,
+ hasThumbnail = hasThumbnail,
+ activeRevisionId = activeRevisionId,
+ xAttr = xAttr,
+ shareUrlId = shareUrlId,
+ contentKeyPacket = contentKeyPacket,
+ contentKeyPacketSignature = contentKeyPacketSignature,
+ )
+}
+
+fun NullableFolder(parentId: FolderId, filename: String = "test-folder"): Folder {
+ return NullableFolder(id = FolderId(parentId.shareId, filename), parentId = parentId)
+}
+
+fun NullableFolder(
+ id: FolderId,
+ parentId: FolderId,
+ name: String = "",
+ size: Bytes = Bytes(0L),
+ lastModified: TimestampS = TimestampS(0L),
+ mimeType: String = "",
+ isShared: Boolean = false,
+ key: String = "",
+ passphrase: String = "",
+ passphraseSignature: String = "",
+ numberOfAccesses: Long = 0,
+ shareUrlExpirationTime: TimestampS? = null,
+ uploadedBy: String = "",
+ isFavorite: Boolean = false,
+ attributes: Attributes = Attributes(0L),
+ permissions: Permissions = Permissions(0L),
+ state: Link.State = Link.State.ACTIVE,
+ nameSignatureEmail: String? = null,
+ hash: String = "",
+ expirationTime: TimestampS? = null,
+ nodeKey: String = "",
+ nodePassphrase: String = "",
+ nodePassphraseSignature: String = "",
+ signatureAddress: String = "",
+ creationTime: TimestampS = TimestampS(0L),
+ trashedTime: TimestampS? = null,
+ xAttr: String? = null,
+ shareUrlId: ShareUrlId? = null,
+ nodeHashKey: String = "",
+): Folder {
+ return Link.Folder(
+ id = id,
+ parentId = parentId,
+ name = name,
+ size = size,
+ lastModified = lastModified,
+ mimeType = mimeType,
+ isShared = isShared,
+ key = key,
+ passphrase = passphrase,
+ passphraseSignature = passphraseSignature,
+ numberOfAccesses = numberOfAccesses,
+ shareUrlExpirationTime = shareUrlExpirationTime,
+ uploadedBy = uploadedBy,
+ isFavorite = isFavorite,
+ attributes = attributes,
+ permissions = permissions,
+ state = state,
+ nameSignatureEmail = nameSignatureEmail,
+ hash = hash,
+ expirationTime = expirationTime,
+ nodeKey = nodeKey,
+ nodePassphrase = nodePassphrase,
+ nodePassphraseSignature = nodePassphraseSignature,
+ signatureAddress = signatureAddress,
+ creationTime = creationTime,
+ trashedTime = trashedTime,
+ xAttr = xAttr,
+ shareUrlId = shareUrlId,
+ nodeHashKey = nodeHashKey
+ )
+}
\ No newline at end of file
diff --git a/drive/link/data/build.gradle.kts b/drive/link/data/build.gradle.kts
index 907c8892..5f6a550d 100644
--- a/drive/link/data/build.gradle.kts
+++ b/drive/link/data/build.gradle.kts
@@ -32,5 +32,6 @@ driveModule(
implementation(project(":drive:share:data"))
implementation(libs.retrofit)
androidTestImplementation(libs.bundles.core)
+ androidTestImplementation(project(":drive:db"))
kaptAndroidTest(libs.androidx.room.compiler)
}
diff --git a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt b/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt
index 02c71359..996ae251 100644
--- a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt
+++ b/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/LinkWithPropertiesTest.kt
@@ -18,11 +18,13 @@
package me.proton.core.drive.link.data.db
import android.content.Context
+import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
+import me.proton.android.drive.db.DriveDatabase
import me.proton.core.account.data.entity.AccountEntity
import me.proton.core.account.domain.entity.AccountState
import me.proton.core.domain.entity.UserId
@@ -40,12 +42,12 @@ import java.io.IOException
@RunWith(AndroidJUnit4::class)
class LinkWithPropertiesTest {
- private lateinit var db: TestDatabase
+ private lateinit var db: DriveDatabase
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext()
- db = buildDatabase(context)
+ db = Room.inMemoryDatabaseBuilder(context, DriveDatabase::class.java).build()
runBlocking { prepareDb(db) }
}
@@ -93,10 +95,10 @@ class LinkWithPropertiesTest {
val link = insertAndGetLink(LinkWithProperties(testParentLink, testFolderProperties(testParentLink.id)))
// endregion
// region When
- db.linkDao().delete(link)
+ db.linkDao.delete(link)
// endregion
//region Then
- assertFalse(db.linkDao().hasLinkEntity(testParentLink.userId, testParentLink.shareId, testParentLink.id).first())
+ assertFalse(db.linkDao.hasLinkEntity(testParentLink.userId, testParentLink.shareId, testParentLink.id).first())
// endregion
}
@@ -107,14 +109,14 @@ class LinkWithPropertiesTest {
insertAndGetLink(LinkWithProperties(testLink, testFileProperties(testLink.id)))
// endregion
// region When
- db.linkDao().delete(parentLink)
+ db.linkDao.delete(parentLink)
// endregion
//region Then
- assertFalse(db.linkDao().hasLinkEntity(testLink.userId, testLink.shareId, testLink.id).first())
+ assertFalse(db.linkDao.hasLinkEntity(testLink.userId, testLink.shareId, testLink.id).first())
// endregion
}
- private suspend fun insertAndGetLink(linkWithProperties: LinkWithProperties) = with (db.linkDao()) {
+ private suspend fun insertAndGetLink(linkWithProperties: LinkWithProperties) = with (db.linkDao) {
insertOrUpdate(linkWithProperties)
getLinkWithPropertiesFlow(
userId = linkWithProperties.link.userId,
@@ -125,9 +127,9 @@ class LinkWithPropertiesTest {
.first()
}
- private suspend fun prepareDb(db: TestDatabase) {
+ private suspend fun prepareDb(db: DriveDatabase) {
db.accountDao().insertOrUpdate(testAccount)
- db.shareDao().insertOrUpdate(testShare)
+ db.shareDao.insertOrUpdate(testShare)
}
private val testAccount =
diff --git a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt b/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt
deleted file mode 100644
index 120e5d36..00000000
--- a/drive/link/data/src/androidTest/kotlin/me/proton/core/drive/link/data/db/TestDatabase.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (c) 2021-2023 Proton AG.
- * This file is part of Proton Core.
- *
- * Proton Core is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * Proton Core 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 Proton Core. If not, see .
- */
-package me.proton.core.drive.link.data.db
-
-import android.content.Context
-import androidx.room.Database
-import androidx.room.Room
-import androidx.room.RoomDatabase
-import androidx.room.TypeConverters
-import me.proton.core.account.data.db.AccountDao
-import me.proton.core.account.data.entity.AccountEntity
-import me.proton.core.account.data.entity.SessionEntity
-import me.proton.core.data.room.db.CommonConverters
-import me.proton.core.drive.link.data.db.entity.LinkEntity
-import me.proton.core.drive.link.data.db.entity.LinkFilePropertiesEntity
-import me.proton.core.drive.link.data.db.entity.LinkFolderPropertiesEntity
-import me.proton.core.drive.share.data.db.ShareDao
-import me.proton.core.drive.share.data.db.ShareEntity
-import me.proton.core.user.data.db.UserConverters
-
-@Database(
- entities = [
- AccountEntity::class,
- SessionEntity::class,
- LinkEntity::class,
- LinkFilePropertiesEntity::class,
- LinkFolderPropertiesEntity::class,
- ShareEntity::class,
- ],
- version = 1,
- exportSchema = false,
-)
-@TypeConverters(
- CommonConverters::class,
- UserConverters::class,
-)
-abstract class TestDatabase : RoomDatabase() {
- abstract fun linkDao(): LinkDao
- abstract fun accountDao(): AccountDao
- abstract fun shareDao(): ShareDao
-}
-
-fun buildDatabase(context: Context): TestDatabase =
- Room.inMemoryDatabaseBuilder(
- context, TestDatabase::class.java
- ).build()
diff --git a/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt b/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt
index f193a8e4..f2800b71 100644
--- a/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt
+++ b/drive/link/data/src/main/kotlin/me/proton/core/drive/link/data/api/extension/GetLinkResponses.kt
@@ -18,10 +18,10 @@
package me.proton.core.drive.link.data.api.extension
-import me.proton.core.data.arch.toDataResult
import me.proton.core.domain.arch.DataResult
import me.proton.core.domain.arch.ResponseSource
import me.proton.core.drive.base.data.api.ProtonApiCode
+import me.proton.core.drive.base.domain.extension.toDataResult
import me.proton.core.drive.link.data.api.response.LinkResponses
import me.proton.core.drive.link.domain.entity.LinkId
import me.proton.core.network.domain.ApiException
@@ -53,6 +53,6 @@ inline fun associateResults(
try {
block().mapResults(links)
} catch (e: ApiException) {
- val result = e.error.toDataResult()
+ val result = e.toDataResult()
links.associateWith { result }
}
diff --git a/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt b/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt
index cfb99b55..2cbe155a 100644
--- a/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt
+++ b/drive/navigation-drawer/presentation/src/main/kotlin/me/proton/core/drive/navigationdrawer/presentation/NavigationDrawer.kt
@@ -246,6 +246,7 @@ fun NavigationDrawerListItem(
) {
ProtonListItem(
icon = icon,
+ iconTintColor = ProtonTheme.colors.iconWeak,
title = title,
modifier = modifier
.clickable {
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt
index 37b1eaef..514d5891 100644
--- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/ImagePreview.kt
@@ -27,41 +27,31 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import coil.annotation.ExperimentalCoilApi
import coil.compose.rememberImagePainter
import coil.request.ImageRequest
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
import me.proton.core.compose.theme.ProtonTheme
+import me.proton.core.compose.theme.isNightMode
import me.proton.core.drive.files.preview.R
-import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect
@Composable
-@OptIn(ExperimentalCoilApi::class)
fun ImagePreview(
uri: Uri,
- zoomEffect: Flow,
+ transformationState: TransformationState,
isFullScreen: Boolean,
modifier: Modifier = Modifier,
) {
val backgroundColor by animateColorAsState(
- targetValue = if (isFullScreen) {
+ targetValue = if (isFullScreen && isNightMode().not()) {
MaterialTheme.colors.onBackground
} else {
MaterialTheme.colors.background
@@ -76,37 +66,24 @@ fun ImagePreview(
ImagePreview(
modifier = modifier.background(backgroundColor),
painter = painter,
- zoomEffect = zoomEffect,
+ transformationState = transformationState,
)
}
@Composable
fun ImagePreview(
painter: Painter,
- zoomEffect: Flow,
+ transformationState: TransformationState,
modifier: Modifier = Modifier,
) {
- var scale by remember { mutableStateOf(1f) }
- var offset by remember { mutableStateOf(Offset.Zero) }
-
- LaunchedEffect(LocalContext.current) {
- zoomEffect
- .onEach { zoomEffect ->
- if (zoomEffect is ZoomEffect.Reset) {
- scale = 1f
- offset = Offset.Zero
- }
- }
- .launchIn(this)
- }
-
val state = rememberTransformableState { zoomChange, offsetChange, _ ->
- scale *= zoomChange
- if (scale < 1f) scale = 1f
- offset += offsetChange
+ transformationState.scale = (transformationState.scale * zoomChange)
+ transformationState.addOffset(offsetChange)
}
Box(
- modifier = modifier.fillMaxSize(),
+ modifier = modifier
+ .fillMaxSize()
+ .onPlaced { transformationState.containerLayout = it },
contentAlignment = Alignment.Center
) {
Image(
@@ -114,12 +91,13 @@ fun ImagePreview(
contentDescription = stringResource(id = R.string.content_description_file_preview),
contentScale = ContentScale.Fit,
modifier = modifier
- .fillMaxSize()
+ .onPlaced { transformationState.contentLayout = it }
.graphicsLayer(
- scaleX = scale,
- scaleY = scale,
- translationX = offset.x,
- translationY = offset.y
+ scaleX = transformationState.scale,
+ scaleY = transformationState.scale,
+ translationX = transformationState.offset.x,
+ translationY = transformationState.offset.y,
+ clip = true,
)
.transformable(
state = state
@@ -134,7 +112,7 @@ fun PreviewImagePreview() {
ProtonTheme {
ImagePreview(
uri = Uri.parse("https://protonmail.com/images/media/live/protonmail-shot-decrypt.jpg"),
- zoomEffect = emptyFlow(),
+ transformationState = rememberTransformationState(),
isFullScreen = false,
)
}
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt
index af1d0ee5..e5797cd7 100644
--- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/MediaPreview.kt
@@ -42,6 +42,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import me.proton.core.compose.theme.ProtonTheme
+import me.proton.core.compose.theme.isNightMode
import me.proton.core.drive.base.presentation.extension.conditional
@Composable
@@ -54,7 +55,7 @@ fun MediaPreview(
mediaControllerVisibility: (Boolean) -> Unit = {}
) {
val backgroundColor by animateColorAsState(
- targetValue = if (isFullScreen) {
+ targetValue = if (isFullScreen && isNightMode().not()) {
MaterialTheme.colors.onBackground
} else {
MaterialTheme.colors.background
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt
index 5cb7721d..72f63c33 100644
--- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/PdfPreview.kt
@@ -17,68 +17,90 @@
*/
package me.proton.core.drive.files.preview.presentation.component
+import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.net.Uri
-import androidx.annotation.WorkerThread
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.rememberTransformableState
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
+import androidx.compose.foundation.gestures.transformable
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
-import coil.annotation.ExperimentalCoilApi
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import me.proton.core.compose.theme.ProtonTheme
import me.proton.core.compose.theme.overline
-import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect
+import java.io.IOException
+import kotlin.math.min
+import kotlin.math.roundToInt
@Composable
fun PdfPreview(
uri: Uri,
- zoomEffect: Flow,
+ transformationState: TransformationState,
modifier: Modifier = Modifier,
onRenderFailed: (Throwable) -> Unit,
) {
- var pages by remember { mutableStateOf>(emptyList()) }
val context = LocalContext.current
- LaunchedEffect(Unit) {
- withContext(Dispatchers.IO) {
+
+ val pdfReader by produceState(initialValue = null, uri) {
+ value = PdfReader(context).apply {
try {
- pages = context.renderPdf(uri)
- } catch (t: Throwable) {
- onRenderFailed(t)
+ openPdf(uri)
+ } catch (e: IOException){
+ onRenderFailed(e)
+ }
+ }
+ awaitDispose {
+ launch {
+ value?.close()
}
}
}
- if (pages.isEmpty()) {
+ val reader = pdfReader
+ if (reader == null) {
PdfLoading(modifier)
} else {
PdfPreview(
modifier = modifier,
- zoomEffect = zoomEffect,
- pages = pages,
+ transformationState = transformationState,
+ onRenderFailed = onRenderFailed,
+ reader = reader,
)
}
}
@@ -94,27 +116,73 @@ fun PdfLoading(modifier: Modifier = Modifier) {
}
@Composable
-@OptIn(ExperimentalCoilApi::class)
+@OptIn(ExperimentalFoundationApi::class)
fun PdfPreview(
- zoomEffect: Flow,
- pages: List,
+ transformationState: TransformationState,
+ reader: PdfReader,
+ onRenderFailed: (Throwable) -> Unit,
modifier: Modifier = Modifier,
) {
+ val lazyListState = rememberLazyListState()
+
+ val density = LocalDensity.current.density
+ val maxWidth = with(LocalDensity.current) {
+ LocalConfiguration.current.smallestScreenWidthDp.dp.roundToPx() * 2
+ }
+
LazyColumn(
modifier = modifier
.fillMaxSize()
- .background(Color.White)
+ .onPlaced { transformationState.containerLayout = it }
+ .background(Color.White),
+ userScrollEnabled = !transformationState.hasScale(),
+ state = lazyListState,
+ flingBehavior = rememberSnapFlingBehavior(lazyListState)
) {
- itemsIndexed(pages) { index, item ->
- Column {
- ImagePreview(
- painter = BitmapPainter(item),
- modifier = Modifier.fillParentMaxHeight(),
- zoomEffect = zoomEffect
- )
+ items(reader.pageCount) { index ->
+ var item by remember { mutableStateOf(null) }
+
+ LaunchedEffect(index) {
+ try {
+ item = reader.renderPage(index, density, maxWidth)
+ } catch (e: CancellationException) {
+ throw e
+ } catch (e: Throwable) {
+ onRenderFailed(e)
+ }
+ }
+
+ val transformableState = rememberTransformableState { zoomChange, offsetChange, _ ->
+ transformationState.scale = transformationState.scale * zoomChange
+ transformationState.addOffset(offsetChange)
+ }
+ Box(
+ modifier = Modifier
+ .fillParentMaxSize()
+ .clipToBounds(),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (item != null) {
+ Image(
+ painter = BitmapPainter(item as ImageBitmap),
+ contentDescription = null,
+ modifier = Modifier
+ .onPlaced { transformationState.contentLayout = it }
+ .graphicsLayer(
+ scaleX = transformationState.scale,
+ scaleY = transformationState.scale,
+ translationX = transformationState.offset.x,
+ translationY = transformationState.offset.y,
+ )
+ .transformable(transformableState),
+ )
+ }
PageNumber(
pageNumber = index + 1,
- pageCount = pages.size,
+ pageCount = reader.pageCount,
+ modifier = Modifier
+ .navigationBarsPadding()
+ .align(Alignment.BottomCenter)
)
}
}
@@ -141,27 +209,56 @@ fun PageNumber(
}
}
-@WorkerThread
-fun Context.renderPdf(uri: Uri): List {
- val pfd = contentResolver.openAssetFileDescriptor(uri, "r")?.parcelFileDescriptor ?: return emptyList()
- return PdfRenderer(pfd).use { pdfRenderer ->
- (0 until pdfRenderer.pageCount).map { pageIndex ->
- renderPage(
- pdfRenderer = pdfRenderer,
- pageIndex = pageIndex,
- )
+class PdfReader(private val context: Context) {
+ private var pdfRenderer: PdfRenderer? = null
+
+ val pageCount: Int
+ get() = pdfRenderer?.pageCount ?: -1
+
+ private val mutex = Mutex()
+
+ @SuppressLint("Recycle")
+ suspend fun openPdf(uri: Uri) {
+ withContext(Dispatchers.IO) {
+ pdfRenderer = context.contentResolver.openAssetFileDescriptor(uri, "r")?.let {
+ PdfRenderer(it.parcelFileDescriptor)
+ }
}
}
+
+ suspend fun close() {
+ withContext(Dispatchers.IO) {
+ mutex.withLock {
+ pdfRenderer?.close()
+ pdfRenderer = null
+ }
+ }
+ }
+
+ suspend fun renderPage(
+ pageIndex: Int,
+ density: Float,
+ maxWidth: Int,
+ ): ImageBitmap {
+ return withContext(Dispatchers.IO) {
+ mutex.withLock {
+ val page = pdfRenderer?.openPage(pageIndex)
+ ?: throw IllegalStateException("Open page with index $pageIndex failed.")
+ page.use {
+ val width = min(page.width.fromPtToPx(density), maxWidth)
+ Bitmap.createBitmap(
+ width,
+ page.height * width / page.width,
+ Bitmap.Config.ARGB_8888
+ ).also { bitmap ->
+ page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
+ }.asImageBitmap()
+ }
+ }
+ }
+ }
+
}
-internal fun renderPage(
- pdfRenderer: PdfRenderer,
- pageIndex: Int,
-): ImageBitmap {
- val page = pdfRenderer.openPage(pageIndex) ?: throw IllegalStateException("Open page with index $pageIndex failed.")
- return page.use {
- Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888).also { bitmap ->
- page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
- }.asImageBitmap()
- }
-}
+private fun Int.fromPtToPx(density: Float): Int =
+ (this.toFloat() / 72F * 160F * density).roundToInt()
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt
index 21b17a37..03f83ada 100644
--- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/Preview.kt
@@ -17,13 +17,13 @@
*/
package me.proton.core.drive.files.preview.presentation.component
-import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
@@ -35,7 +35,9 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -45,11 +47,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
@@ -61,7 +63,6 @@ import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.rememberPagerState
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
@@ -80,12 +81,12 @@ import me.proton.core.drive.base.presentation.component.TopAppBar
import me.proton.core.drive.base.presentation.entity.FileTypeCategory
import me.proton.core.drive.base.presentation.extension.conditional
import me.proton.core.drive.base.presentation.extension.debugOnly
+import me.proton.core.drive.base.presentation.extension.isLandscape
import me.proton.core.drive.files.preview.R
import me.proton.core.drive.files.preview.presentation.component.event.PreviewViewEvent
import me.proton.core.drive.files.preview.presentation.component.state.ContentState
import me.proton.core.drive.files.preview.presentation.component.state.PreviewContentState
import me.proton.core.drive.files.preview.presentation.component.state.PreviewViewState
-import me.proton.core.drive.files.preview.presentation.component.state.ZoomEffect
import me.proton.core.util.kotlin.exhaustive
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -98,19 +99,11 @@ import me.proton.core.presentation.R as CorePresentation
fun Preview(
viewState: PreviewViewState,
viewEvent: PreviewViewEvent,
- zoomEffect: Flow,
modifier: Modifier = Modifier,
onPageChanged: FlowCollector? = null,
) {
- val detectTapGestureModifier = Modifier
- .pointerInput(Unit) {
- detectTapGestures(
- onTap = { viewEvent.onSingleTap() },
- onDoubleTap = { viewEvent.onDoubleTap() }
- )
- }
val pagerState = rememberPagerState(initialPage = viewState.currentIndex)
-
+ val userScrollEnabled = remember { mutableStateOf(true) }
val isFullScreen by rememberFlowWithLifecycle(viewState.isFullscreen).collectAsState(false)
onPageChanged?.let {
@@ -133,16 +126,16 @@ fun Preview(
HorizontalPager(
state = pagerState,
count = viewState.items.size,
+ userScrollEnabled = userScrollEnabled.value,
key = { page -> viewState.items[page].key }
) { page ->
PreviewContent(
viewState.items[page],
isFullScreen,
viewEvent,
- zoomEffect,
with(LocalDensity.current) { topBarHeightAnimated.toDp() },
isFocused = pagerState.currentPage == page,
- detectTapGestureModifier,
+ userScrollEnabled,
)
}
AnimatedVisibility(
@@ -150,7 +143,7 @@ fun Preview(
enter = slideInVertically(initialOffsetY = { fullHeight: Int -> -fullHeight }),
exit = slideOutVertically(targetOffsetY = { fullHeight: Int -> -fullHeight }),
) {
- val item = viewState.items.getOrNull(viewState.currentIndex)
+ val item = viewState.items.getOrNull(pagerState.currentPage)
TopAppBar(
modifier = Modifier
.background(appBarGradient)
@@ -160,7 +153,8 @@ fun Preview(
}
.onSizeChanged { size ->
topBarHeight = size.height
- }.testTag(PreviewComponentTestTag.screen),
+ }
+ .testTag(PreviewComponentTestTag.screen),
navigationIcon = painterResource(id = viewState.navigationIconResId),
onNavigationIcon = { viewEvent.onTopAppBarNavigation() },
title = item?.title ?: "",
@@ -178,9 +172,6 @@ fun Preview(
}
}
-private val isLandscape: Boolean @Composable get() =
- LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
-
private val appBarGradient: Brush @Composable get() = Brush.verticalGradient(
colors = listOf(0.8f, 0.7f, 0.6f, 0.5f, 0f).map { alpha ->
ProtonTheme.colors.backgroundNorm.copy(alpha = alpha)
@@ -192,10 +183,9 @@ fun PreviewContent(
item: PreviewViewState.Item,
isFullScreen: Boolean,
viewEvent: PreviewViewEvent,
- zoomEffect: Flow,
topBarHeight: Dp,
isFocused: Boolean,
- modifier: Modifier = Modifier,
+ userScrollEnabled: MutableState,
) {
val contentState by rememberFlowWithLifecycle(item.contentState).collectAsState(
ContentState.Downloading(null)
@@ -215,10 +205,9 @@ fun PreviewContent(
item.category.toComposable(),
isFullScreen,
viewEvent,
- zoomEffect,
topBarHeight,
isFocused,
- modifier,
+ userScrollEnabled
)
ContentState.NotFound -> PreviewNotFound()
is ContentState.Error -> {
@@ -275,21 +264,55 @@ fun PreviewContentAvailable(
previewComposable: PreviewComposable,
isFullScreen: Boolean,
viewEvent: PreviewViewEvent,
- zoomEffect: Flow,
topBarHeight: Dp,
isFocused: Boolean,
+ userScrollEnabled: MutableState,
+ transformationState : TransformationState = rememberTransformationState(),
+ onDoubleTap : () -> Unit = { transformationState.scale = 2F },
modifier: Modifier = Modifier,
) {
+ val dragEnable = transformationState.hasScale()
+
+ if (isFocused) {
+ DisposableEffect(dragEnable) {
+ userScrollEnabled.value = !dragEnable
+ onDispose {
+ userScrollEnabled.value = true
+ }
+ }
+ }
+
+ val pointerInputModifier = modifier
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onTap = { viewEvent.onSingleTap() },
+ onDoubleTap = {
+ if (transformationState.scale == 1F) {
+ onDoubleTap()
+ } else {
+ transformationState.scale = 1F
+ transformationState.offset = Offset.Zero
+ }
+ }
+ )
+ }
+ .pointerInput(Unit, dragEnable) {
+ if (!dragEnable) return@pointerInput
+ detectDragGestures { _, dragAmount ->
+ transformationState.addOffset(dragAmount)
+ }
+ }
+
when (previewComposable) {
PreviewComposable.Image -> ImagePreview(
- modifier = modifier,
+ modifier = pointerInputModifier,
uri = contentState.uri,
- zoomEffect = zoomEffect,
+ transformationState = transformationState,
isFullScreen = isFullScreen,
)
PreviewComposable.Sound,
PreviewComposable.Video -> MediaPreview(
- modifier = modifier,
+ modifier = pointerInputModifier,
uri = contentState.uri,
isFullScreen = isFullScreen,
play = isFocused,
@@ -297,13 +320,13 @@ fun PreviewContentAvailable(
)
PreviewComposable.Pdf -> PdfPreview(
uri = contentState.uri,
- modifier = modifier.padding(top = topBarHeight),
- zoomEffect = zoomEffect,
+ modifier = pointerInputModifier.padding(top = topBarHeight),
+ transformationState = transformationState,
onRenderFailed = viewEvent.onRenderFailed,
)
PreviewComposable.Text -> TextPreview(
uri = contentState.uri,
- modifier = modifier.padding(top = topBarHeight),
+ modifier = pointerInputModifier.padding(top = topBarHeight),
onRenderFailed = viewEvent.onRenderFailed,
)
PreviewComposable.Unknown -> UnknownPreview()
@@ -448,11 +471,9 @@ fun PreviewPreviewLoadingState() {
override val onTopAppBarNavigation: () -> Unit = {}
override val onMoreOptions: () -> Unit = {}
override val onSingleTap: () -> Unit = {}
- override val onDoubleTap: () -> Unit = {}
override val onRenderFailed: (Throwable) -> Unit = {}
override val mediaControllerVisibility: (Boolean) -> Unit = {}
},
- zoomEffect = emptyFlow(),
)
}
}
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt
new file mode 100644
index 00000000..8320e446
--- /dev/null
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/TransformationState.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.files.preview.presentation.component
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.unit.IntSize
+
+@Composable
+fun rememberTransformationState(
+ initialScale: Float = 1F,
+ initialOffset: Offset = Offset.Zero,
+ initialMinScale: Float = 1F,
+ initialMaxScale: Float = 4F,
+): TransformationState {
+ return rememberSaveable(saver = TransformationState.Saver) {
+ TransformationState(
+ initialScale = initialScale,
+ initialOffset = initialOffset,
+ initialMinScale = initialMinScale,
+ initialMaxScale = initialMaxScale,
+ )
+ }
+}
+
+class TransformationState(
+ private val initialScale: Float = 1F,
+ private val initialOffset: Offset = Offset.Zero,
+ val initialMinScale: Float = 1F,
+ val initialMaxScale: Float = 4F,
+) {
+
+ var containerLayout: LayoutCoordinates? = null
+ var contentLayout: LayoutCoordinates? = null
+ set(value) {
+ // Do not take LayoutCoordinates with a size of zero it would stop updating the offset
+ if(value?.size != IntSize.Zero) {
+ field = value
+ }
+ }
+
+ private var _scale by mutableStateOf(initialScale)
+ var scale
+ get() = _scale
+ set(value) {
+ _scale = value.coerceIn(minScale, maxScale)
+ }
+ var offset by mutableStateOf(initialOffset)
+ var minScale by mutableStateOf(initialMinScale)
+ var maxScale by mutableStateOf(initialMaxScale)
+
+ fun hasScale() = scale > 1F
+
+ fun addOffset(dragAmount: Offset) {
+ val containerSize = containerLayout?.size
+ val contentSize = contentLayout?.size
+ if (containerSize == null || contentSize == null) {
+ // let the offset go outside bounds sizes are not set
+ offset += dragAmount
+ } else {
+ val horizontalLimit = ((contentSize.width * scale - containerSize.width) / 2F)
+ .coerceAtLeast(0F)
+ val verticalLimit = ((contentSize.height * scale - containerSize.height) / 2F)
+ .coerceAtLeast(0F)
+
+ val offsetX = (offset.x + dragAmount.x)
+ .coerceIn(minimumValue = -horizontalLimit, maximumValue = horizontalLimit)
+ val offsetY = (offset.y + dragAmount.y)
+ .coerceIn(minimumValue = -verticalLimit, maximumValue = verticalLimit)
+ offset = Offset(x = offsetX, y = offsetY)
+ }
+ }
+
+ companion object {
+ val Saver: Saver = listSaver(
+ save = {
+ listOf(
+ it.scale,
+ it.offset.x,
+ it.offset.y,
+ it.minScale,
+ it.maxScale,
+ )
+ },
+ restore = {
+ TransformationState(
+ initialScale = it[0],
+ initialOffset = Offset(it[1], it[2]),
+ initialMinScale = it[3],
+ initialMaxScale = it[4],
+ )
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt
index 8c66329c..2fb6a485 100644
--- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/event/PreviewViewEvent.kt
@@ -22,7 +22,6 @@ interface PreviewViewEvent {
val onTopAppBarNavigation: () -> Unit
val onMoreOptions: () -> Unit
val onSingleTap: () -> Unit
- val onDoubleTap: () -> Unit
val onRenderFailed: (Throwable) -> Unit
val mediaControllerVisibility: (Boolean) -> Unit
}
diff --git a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt
index 544523bf..e3fd1510 100644
--- a/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt
+++ b/drive/preview/src/main/kotlin/me/proton/core/drive/files/preview/presentation/component/state/PreviewViewState.kt
@@ -40,10 +40,6 @@ data class PreviewViewState(
)
}
-sealed class ZoomEffect {
- object Reset : ZoomEffect()
-}
-
sealed interface PreviewContentState {
object Loading : PreviewContentState
object Empty : PreviewContentState
diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt
index baad9947..be6a0dc6 100644
--- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt
+++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/Settings.kt
@@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -46,10 +47,12 @@ import me.proton.core.drive.settings.presentation.component.DebugSettings
import me.proton.core.drive.settings.presentation.component.ExternalSettingsEntry
import me.proton.core.drive.settings.presentation.component.ThemeChooserDialog
import me.proton.core.drive.settings.presentation.event.SettingsViewEvent
+import me.proton.core.drive.settings.presentation.extension.toString
import me.proton.core.drive.settings.presentation.state.LegalLink
import me.proton.core.drive.settings.presentation.state.SettingsViewState
import me.proton.core.usersettings.presentation.compose.view.CrashReportSettingToggleItem
import me.proton.core.usersettings.presentation.compose.view.TelemetrySettingToggleItem
+import kotlin.time.Duration.Companion.seconds
import me.proton.core.drive.base.presentation.R as BasePresentation
import me.proton.core.presentation.R as CorePresentation
@@ -80,6 +83,24 @@ fun Settings(
)
Column(Modifier.verticalScroll(rememberScrollState())) {
+ ProtonSettingsHeader(title = R.string.settings_section_security)
+
+ ProtonSettingsItem(
+ name = stringResource(id = R.string.settings_app_lock),
+ hint = stringResource(id = viewState.appAccessSubtitleResId),
+ ) {
+ viewEvent.onAppAccess()
+ }
+
+ if (viewState.isAutoLockDurationsVisible) {
+ ProtonSettingsItem(
+ name = stringResource(id = R.string.settings_auto_lock),
+ hint = viewState.autoLockDuration.toString(LocalContext.current),
+ ) {
+ viewEvent.onAutoLockDurations()
+ }
+ }
+
ProtonSettingsHeader(title = R.string.settings_section_appearance_settings)
ProtonSettingsItem(
@@ -89,14 +110,7 @@ fun Settings(
showThemeDialog = true
}
- if (viewState.isAppLockEnabled) {
- ProtonSettingsHeader(title = R.string.settings_section_app_settings)
- ProtonSettingsItem(
- name = stringResource(id = R.string.settings_pin_biomtric_lock_entry),
- hint = stringResource(R.string.settings_pin_biomtric_lock_status_off),
- )
- }
if (viewState.legalLinks.isNotEmpty()) {
ProtonSettingsHeader(title = R.string.settings_section_about)
@@ -158,11 +172,16 @@ private fun SettingsPreview() {
),
availableStyles = emptyList(),
currentStyle = BasePresentation.string.common_cancel_action,
+ appAccessSubtitleResId = BasePresentation.string.common_cancel_action,
+ isAutoLockDurationsVisible = true,
+ autoLockDuration = 0.seconds,
),
viewEvent = SettingsViewEvent(
navigateBack = {},
onLinkClicked = {},
onThemeStyleChanged = {},
+ onAppAccess = {},
+ onAutoLockDurations = {}
)
)
}
diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt
index 15b87c63..7e15f65f 100644
--- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt
+++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/component/SettingsEntry.kt
@@ -206,7 +206,7 @@ private fun PreviewExternalSettingsEntry() {
modifier = Modifier.background(ProtonTheme.colors.backgroundNorm),
link = LegalLink.External(
text = BasePresentation.string.title_app,
- url = R.string.settings_section_app_settings,
+ url = R.string.settings_section_security,
)) {
}
}
diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt
index a6b22dc5..3d24b1f1 100644
--- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt
+++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/event/SettingsViewEvent.kt
@@ -24,4 +24,6 @@ data class SettingsViewEvent(
val navigateBack: () -> Unit,
val onLinkClicked: (LegalLink) -> Unit,
val onThemeStyleChanged: (Int) -> Unit,
+ val onAppAccess: () -> Unit,
+ val onAutoLockDurations: () -> Unit,
)
diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt
new file mode 100644
index 00000000..32f233ed
--- /dev/null
+++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/extension/Duration.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.settings.presentation.extension
+
+import android.content.Context
+import me.proton.core.drive.base.presentation.extension.quantityString
+import kotlin.time.Duration
+import me.proton.core.drive.settings.R as SettingsPresentation
+
+fun Duration.toString(context: Context): String = when {
+ inWholeSeconds == 0L -> context.getString(SettingsPresentation.string.settings_auto_lock_durations_immediately)
+ inWholeSeconds < 60 -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_seconds, inWholeSeconds.toInt())
+ inWholeMinutes < 60 -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_minutes, inWholeMinutes.toInt())
+ inWholeHours < 24 -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_hours, inWholeHours.toInt())
+ else -> context.quantityString(SettingsPresentation.plurals.settings_auto_lock_durations_days, inWholeDays.toInt())
+}
diff --git a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt
index b30910ff..37c4e0c5 100644
--- a/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt
+++ b/drive/settings/src/main/java/me/proton/core/drive/settings/presentation/state/SettingsViewState.kt
@@ -21,6 +21,7 @@ package me.proton.core.drive.settings.presentation.state
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import me.proton.core.drive.settings.presentation.component.DebugSettingsStateAndEvent
+import kotlin.time.Duration
data class SettingsViewState(
@DrawableRes val navigationIcon: Int,
@@ -30,7 +31,9 @@ data class SettingsViewState(
val availableStyles: List,
@StringRes val currentStyle: Int,
val debugSettingsStateAndEvent: DebugSettingsStateAndEvent? = null,
- val isAppLockEnabled: Boolean = false,
+ @StringRes val appAccessSubtitleResId: Int,
+ val isAutoLockDurationsVisible: Boolean,
+ val autoLockDuration: Duration,
)
sealed class LegalLink(
diff --git a/drive/settings/src/main/res/values/strings.xml b/drive/settings/src/main/res/values/strings.xml
index 6756fd7c..122cf3d8 100644
--- a/drive/settings/src/main/res/values/strings.xml
+++ b/drive/settings/src/main/res/values/strings.xml
@@ -17,10 +17,27 @@
-->
-
- Off
- App settings
+ App Lock
+ Security
About
+ Automatically lock the app
+ Immediately
+
+ - After %1$d second
+ - After %1$d seconds
+
+
+ - After %1$d minute
+ - After %1$d minutes
+
+
+ - After %1$d hour
+ - After %1$d hours
+
+
+ - After %1$d day
+ - After %1$d days
+
+
+
diff --git a/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt
new file mode 100644
index 00000000..14fa858c
--- /dev/null
+++ b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/di/TestModule.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.share.data.test.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import me.proton.core.drive.share.data.test.repository.StubbedShareRepository
+import me.proton.core.drive.share.domain.repository.ShareRepository
+import javax.inject.Singleton
+
+@Suppress("unused")
+@Module
+@InstallIn(SingletonComponent::class)
+interface TestModule {
+ @Binds
+ @Singleton
+ fun bindsShareRepository(manager: StubbedShareRepository): ShareRepository
+}
diff --git a/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt
new file mode 100644
index 00000000..c1e664d8
--- /dev/null
+++ b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/nullable/NullableShare.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+@file:Suppress("FunctionName")
+/*
+ * Copyright (c) 2023 Proton Technologies AG
+ * This file is part of Proton Technologies AG and ProtonCore.
+ *
+ * ProtonCore is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ProtonCore 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 ProtonCore. If not, see .
+ */
+package me.proton.core.drive.share.data.test.nullable
+
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.domain.entity.TimestampS
+import me.proton.core.drive.share.domain.entity.Share
+import me.proton.core.drive.share.domain.entity.ShareId
+import me.proton.core.drive.volume.domain.entity.VolumeId
+import me.proton.core.user.domain.entity.AddressId
+
+fun NullableShare(
+ id: ShareId = ShareId(UserId("user-id"), "share-id"),
+ volumeId: VolumeId = VolumeId("volume-id"),
+ rootLinkId: String = "",
+ addressId: AddressId? = null,
+ isMain: Boolean = false,
+ isLocked: Boolean = false,
+ key: String = "",
+ passphrase: String = "",
+ passphraseSignature: String = "",
+ creationTime: TimestampS? = null
+) = Share(
+ id = id,
+ volumeId = volumeId,
+ rootLinkId = rootLinkId,
+ addressId = addressId,
+ isMain = isMain,
+ isLocked = isLocked,
+ key = key,
+ passphrase = passphrase,
+ passphraseSignature = passphraseSignature,
+ creationTime = creationTime
+)
\ No newline at end of file
diff --git a/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt
new file mode 100644
index 00000000..94bf52e0
--- /dev/null
+++ b/drive/share/data-test/src/main/kotlin/me/proton/core/drive/share/data/test/repository/StubbedShareRepository.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2023 Proton AG.
+ * This file is part of Proton Core.
+ *
+ * Proton Core is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Proton Core 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 Proton Core. If not, see .
+ */
+
+package me.proton.core.drive.share.data.test.repository
+/*
+ * Copyright (c) 2023 Proton Technologies AG
+ * This file is part of Proton Technologies AG and ProtonCore.
+ *
+ * ProtonCore is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * ProtonCore 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 ProtonCore. If not, see .
+ */
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import me.proton.core.domain.arch.DataResult
+import me.proton.core.domain.entity.UserId
+import me.proton.core.drive.base.domain.extension.asSuccess
+import me.proton.core.drive.share.data.test.nullable.NullableShare
+import me.proton.core.drive.share.domain.entity.Share
+import me.proton.core.drive.share.domain.entity.ShareId
+import me.proton.core.drive.share.domain.entity.ShareInfo
+import me.proton.core.drive.share.domain.repository.ShareRepository
+import me.proton.core.drive.volume.domain.entity.VolumeId
+import javax.inject.Inject
+
+class StubbedShareRepository @Inject constructor() : ShareRepository {
+
+ private val sharesFlow = MutableStateFlow(
+ listOf(
+ NullableShare(
+ mainShareId,
+ isMain = true,
+ key = "key",
+ passphrase = "passphrase",
+ passphraseSignature = "passphraseSignature",
+ )
+ )
+ )
+
+ override fun getSharesFlow(userId: UserId): Flow>> =
+ filterShares { share -> share.id.userId == userId }.map { it.asSuccess }
+
+ override fun getSharesFlow(userId: UserId, volumeId: VolumeId): Flow>> =
+ filterShares { share -> share.id.userId == userId && share.volumeId == volumeId }.map { it.asSuccess }
+
+ override suspend fun hasShares(userId: UserId): Boolean {
+ return fetchShares(userId).isNotEmpty()
+ }
+
+ override suspend fun hasShares(userId: UserId, volumeId: VolumeId): Boolean {
+ return filterShares { share -> share.id.userId == userId }.first().isNotEmpty()
+ }
+
+ override suspend fun fetchShares(userId: UserId): List {
+ return filterShares { share -> share.id.userId == userId }.first()
+ }
+
+ override fun getShareFlow(shareId: ShareId): Flow