This commit is contained in:
Anatoly Rosencrantz
2023-04-04 13:23:14 +03:00
parent 1245db08ef
commit 2c8d2dfeaf
299 changed files with 21811 additions and 4638 deletions
+14 -15
View File
@@ -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
+1 -1
View File
@@ -1,3 +1,3 @@
<component name="DependencyValidationManager">
<scope name="proton-core" pattern="[ProtonDrive.drive*]:*..*||file[ProtonDrive.drive*]:AndroidManifest.xml" />
<scope name="proton-core" pattern="[ProtonDrive.drive*]:*..*||file[ProtonDrive.drive*]:AndroidManifest.xml||file[ProtonDrive.drive*]:*.properties" />
</component>
+23
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
plugins {
id("com.android.library")
}
driveModule(includeSubmodules = true)
+32
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<manifest package="me.proton.android.drive.lock.data" />
@@ -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 <https://www.gnu.org/licenses/>.
*/
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())
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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))
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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))
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLockEntity>() {
@Query("""
SELECT EXISTS(SELECT * FROM AppLockEntity)
""")
abstract suspend fun hasAppLock(): Boolean
@Query("""
SELECT EXISTS(SELECT * FROM AppLockEntity)
""")
abstract fun hasAppLockFlow(): Flow<Boolean>
@Query("""
SELECT * FROM AppLockEntity LIMIT 1
""")
abstract suspend fun getAppLock(): AppLockEntity
@Query("""
DELETE FROM AppLockEntity
""")
abstract suspend fun deleteAppLock()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AutoLockDurationEntity>() {
@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<AutoLockDurationEntity?>
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<EnableAppLockEntity>() {
@Query(
"""
SELECT EXISTS(SELECT * FROM EnableAppLockEntity WHERE `key` = :key)
"""
)
abstract suspend fun hasEnableAppLock(key: String): Boolean
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<LockEntity>() {
@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)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T> unlock(
key: String,
block: suspend (passphrase: ByteArray) -> T,
): Result<T> = 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<ByteArray> = 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"
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T> unlock(
key: String,
block: suspend (passphrase: ByteArray) -> T,
): Result<T> = 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<ByteArray> = 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"
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Boolean> = appLockRepository.hasAppLockKeyFlow()
.stateIn(coroutineScope, SharingStarted.Eagerly, false)
private val appKey = MutableStateFlow<SecretKey?>(null)
private val _locked: Flow<Boolean> = appKey.map { secretKey -> secretKey == null }
override val locked: StateFlow<Boolean> = 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<Boolean> = coRunCatching {
unlock(secretKey)
appLockRepository.insertAppLockKey(appLock)
appLockRepository.insertOrUpdateEnableAppLockTimestamp(System.currentTimeMillis())
true
}
override suspend fun disable(): Result<Boolean> = coRunCatching {
appLockRepository.deleteAppLockKey()
lock()
true
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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"
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<BiometricPrompt.AuthenticationResult>
fun getLockState(): LockState
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AuthenticationListener, Unit>()
private var activity: WeakReference<FragmentActivity> = 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<BiometricPrompt.AuthenticationResult> = coRunCatching {
cryptoObject?.let {
buildBiometricPrompt().authenticate(biometricPromptInfo(title, subtitle), cryptoObject)
} ?: buildBiometricPrompt().authenticate(biometricPromptInfo(title, subtitle))
// await result
suspendCancellableCoroutine<Result<BiometricPrompt.AuthenticationResult>> { 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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Boolean> = 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<Duration> = 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"
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SecretKey> = coRunCatching {
lock.unlock(key) { passphrase ->
PgpSecretKey(
passphrase = PlainByteArray(passphrase),
lockedKey = key,
cryptoContext = cryptoContext,
)
}.getOrThrow()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SecretKey> = 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"
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLock> = coRunCatching {
require(secretKey is PgpSecretKey)
AppLock(
key = secretKey.lockedKey,
type = appLockType,
)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> = emptyList(),
): OneTimeWorkRequest =
OneTimeWorkRequest.Builder(AppLockWorker::class.java)
.setInitialDelay(runAfter.inWholeSeconds, TimeUnit.SECONDS)
.addTags(tags)
.build()
}
}
+29
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
plugins {
id("com.android.library")
}
driveModule(
hilt = true,
serialization = true,
) {
api(project(":drive:link:domain"))
api(project(":drive:crypto-base:domain"))
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<manifest package="me.proton.android.drive.lock.domain" />
@@ -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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.lock.domain.entity
data class AppLock(
val key: String,
val type: AppLockType,
)
@@ -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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.lock.domain.entity
enum class AppLockType {
SYSTEM,
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<T> unlock(key: String, block: suspend (passphrase: ByteArray) -> T): Result<T>
/**
* Locks [passphrase] so that it's safe to store.
*/
suspend fun lock(passphrase: ByteArray): Result<ByteArray>
/**
* 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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Boolean>
val enabled: StateFlow<Boolean>
suspend fun isLocked(): Boolean
suspend fun isEnabled(): Boolean
suspend fun unlock(appKey: SecretKey)
suspend fun lock()
suspend fun enable(secretKey: SecretKey, appLock: AppLock): Result<Boolean>
suspend fun disable(): Result<Boolean>
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.lock.domain.manager
interface AutoLockManager {
suspend fun autoLock()
fun cancelAutoLock()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Boolean>
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<Duration>
suspend fun insertOrUpdateAutoLockDuration(duration: Duration)
suspend fun hasEnableAppLockTimestamp(): Boolean
suspend fun insertOrUpdateEnableAppLockTimestamp(timestamp: Long)
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SecretKey>
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLockType, Lock>,
) {
suspend operator fun invoke(
lockType: AppLockType = AppLockType.SYSTEM,
userAuthenticationRequired: Boolean = true,
): Result<Unit> = coRunCatching {
if (appLockManager.isEnabled()) {
requireNotNull(locks[lockType]).disable(userAuthenticationRequired)
appLockManager.disable().getOrThrow()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLockType, Lock>,
) {
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SecretKey>
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLock>
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Duration> = flow {
if (appLockRepository.hasAutoLockDuration().not()) {
emit(configurationProvider.autoLockDurations.first())
}
emitAll(appLockRepository.getAutoLockDuration())
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLockType, Lock>,
) {
operator fun invoke(appLockType: AppLockType = AppLockType.SYSTEM): LockState =
requireNotNull(locks[appLockType]).getLockState()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppLockType, Lock>,
private val buildAppKey: BuildAppKey,
) {
suspend operator fun invoke(): Result<Unit> = coRunCatching {
if (appLockManager.isEnabled() && appLockManager.isLocked()) {
val appLockKey = appLockRepository.getAppLockKey()
appLockManager.unlock(
buildAppKey(appLockKey.key, requireNotNull(locks[appLockKey.type])).getOrThrow()
)
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Unit> = coRunCatching {
require(configurationProvider.autoLockDurations.contains(duration)) {
"Only values from ConfigurationProvider#autoLockDurations are allowed"
}
appLockRepository.insertOrUpdateAutoLockDuration(duration)
}
}
+30
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
plugins {
id("com.android.library")
}
driveModule(
hilt = true,
compose = true,
) {
api(project(":app-lock:domain"))
implementation(project(":drive:base:presentation"))
implementation(libs.accompanist.drawablepainter)
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<manifest package="me.proton.android.drive.lock.presentation" />
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Boolean>,
primaryAccount: Flow<Account?>,
content: @Composable () -> Unit,
) {
var isLocked by remember { mutableStateOf(false) }
var userId by remember { mutableStateOf<UserId?>(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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<UnlockViewModel>()
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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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")
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.lock.presentation.viewevent
import me.proton.core.domain.entity.UserId
interface UnlockViewEvent {
val onShowBiometric: () -> Unit
val onSignOut: (UserId?) -> Unit
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<resources>
<item name="welcome_header" type="drawable">@drawable/welcome_header_dark_land</item>
</resources>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<resources>
<item name="welcome_header" type="drawable">@drawable/welcome_header_light_land</item>
<item name="welcome_header_light" type="drawable">@drawable/welcome_header_light_land</item>
<item name="welcome_header_dark" type="drawable">@drawable/welcome_header_dark_land</item>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<resources>
<item name="welcome_header" type="drawable">@drawable/welcome_header_dark</item>
</resources>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<resources>
<item name="welcome_header" type="drawable">@drawable/welcome_header_light</item>
</resources>
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<manifest package="me.proton.android.drive.lock" />
+3 -2
View File
@@ -16,7 +16,6 @@
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
*/
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)
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"/>
</manifest>
+31 -2
View File
@@ -126,6 +126,17 @@
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="me.proton.android.drive.initializer.MainInitializer"
android:value="androidx.startup"
tools:node="remove"/>
<meta-data
android:name="me.proton.android.drive.initializer.DocumentsProviderInitializer"
android:value="androidx.startup" />
<meta-data
android:name="me.proton.android.drive.initializer.AutoLockInitializer"
android:value="androidx.startup"
tools:node="remove"/><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.android.drive.initializer.NotificationChannelInitializer"
android:value="androidx.startup" />
@@ -136,9 +147,18 @@
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
<meta-data
android:name="me.proton.core.humanverification.presentation.HumanVerificationInitializer"
android:value="androidx.startup"
tools:node="remove" /><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.android.drive.initializer.WorkManagerInitializer"
android:value="androidx.startup" />
android:value="androidx.startup"
tools:node="remove" /><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.core.plan.presentation.UnredeemedPurchaseInitializer"
android:value="androidx.startup"
tools:node="remove" /><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.android.drive.initializer.AccountStateHandlerInitializer"
android:value="androidx.startup" />
@@ -147,7 +167,16 @@
android:value="androidx.startup" />
<meta-data
android:name="me.proton.android.drive.initializer.EventManagerInitializer"
android:value="androidx.startup" />
android:value="androidx.startup"
tools:node="remove" /><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.core.auth.presentation.MissingScopeInitializer"
android:value="androidx.startup"
tools:node="remove" /><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.core.network.presentation.init.UnAuthSessionFetcherInitializer"
android:value="androidx.startup"
tools:node="remove" /><!-- Initialized by MainInitializer -->
<meta-data
android:name="me.proton.android.drive.initializer.LoggerInitializer"
android:value="androidx.startup" />
@@ -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)
}
}
@@ -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<Migration>()
fun buildDatabase(context: Context): AppDatabase =
databaseBuilder<AppDatabase>(context, "db-drive")
databaseBuilder<AppDatabase>(context, "db-app")
.apply { migrations.forEach { addMigrations(it) } }
.build()
}
@@ -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
}
@@ -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
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
@@ -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)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Unit> {
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<Class<out Initializer<*>>> = listOf(
LoggerInitializer::class.java,
WorkManagerInitializer::class.java,
)
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AutoLockInitializerEntryPoint {
val appLifecycleProvider: AppLifecycleProvider
val appLockManager: AppLockManager
val autoLockManager: AutoLockManager
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Unit> {
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<Class<out Initializer<*>>> = listOf(
LoggerInitializer::class.java,
)
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AutoLockInitializerEntryPoint {
val appLifecycleProvider: AppLifecycleProvider
val appLockManager: AppLockManager
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Unit> {
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)
}
}
}
}
@@ -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())
}
}
}
@@ -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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AutoLockDurationsViewModel>()
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) {}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SystemAccessDialogViewModel>()
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,
)
}
)
}
@@ -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<Unit>,
deepLinkIntent: SharedFlow<Intent>,
locked: Flow<Boolean>,
primaryAccount: Flow<Account?>,
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() }
)
}
@@ -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}") {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<AppAccessViewModel>()
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 = {}
)
}
}
@@ -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 = {
@@ -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"
}
@@ -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
@@ -107,7 +107,6 @@ fun PreviewScreen(
navigateBack = navigateBack,
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
),
zoomEffect = viewModel.zoomEffect,
modifier = modifier,
) { page ->
viewModel.onPageChanged(page)
@@ -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<SettingsViewModel>()
@@ -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(
@@ -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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.ui.viewevent
interface AppAccessViewEvent {
val onDisable: () -> Unit
val onSystem: () -> Unit
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
package me.proton.android.drive.ui.viewevent
import kotlin.time.Duration
interface AutoLockDurationsViewEvent {
val onDuration: (Duration) -> Unit
}

Some files were not shown because too many files have changed in this diff Show More