mirror of
https://github.com/ProtonDriveApps/android-drive.git
synced 2026-05-15 09:50:34 +00:00
2.4.0
This commit is contained in:
+93
-8
@@ -12,10 +12,10 @@ variables:
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "candidate"
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "candidate"
|
||||
|
||||
before_script:
|
||||
# We must keep these variables here. We can't do it inside the entrypoint, as idk how but
|
||||
@@ -262,10 +262,12 @@ drive-sorting-presentation-firebase-tests:
|
||||
allow_failure: true
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:all/'
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
|
||||
when: manual
|
||||
|
||||
|
||||
test:firebase:e2e:smoke:
|
||||
extends: .app-firebase-tests
|
||||
when: manual
|
||||
@@ -277,67 +279,128 @@ test:firebase:e2e:smoke:
|
||||
|
||||
test:firebase:e2e:account:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:account/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.account"
|
||||
|
||||
|
||||
test:firebase:e2e:computers:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:computers/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.computers"
|
||||
|
||||
test:firebase:e2e:creatingFolder:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:creatingFolder/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.creatingFolder"
|
||||
|
||||
test:firebase:e2e:deeplink:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:deeplink/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.deeplink"
|
||||
|
||||
test:firebase:e2e:details:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:details/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.details"
|
||||
|
||||
test:firebase:e2e:move:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:move/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.move"
|
||||
DEVICE_CONFIG: "quickTest-2"
|
||||
|
||||
test:firebase:e2e:offline:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:offline/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.offline"
|
||||
|
||||
test:firebase:e2e:photos:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:photos/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.photos"
|
||||
|
||||
test:firebase:e2e:preview:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:preview/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.preview"
|
||||
|
||||
test:firebase:e2e:rename:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:rename/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.rename"
|
||||
|
||||
test:firebase:e2e:settings:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:settings/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.settings"
|
||||
DEVICE_CONFIG: "quickTest-3"
|
||||
|
||||
test:firebase:e2e:share:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:share/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.share"
|
||||
DEVICE_CONFIG: "quickTest-2"
|
||||
|
||||
test:firebase:e2e:subscription:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:subscription/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.subscription"
|
||||
|
||||
test:firebase:e2e:trash:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:trash/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.trash"
|
||||
|
||||
test:firebase:e2e:upload:
|
||||
extends: .app-firebase-tests
|
||||
rules:
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /scope:upload/'
|
||||
- !reference [.app-firebase-tests, rules]
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.upload"
|
||||
|
||||
test:firebase:e2e:photos:
|
||||
extends: .app-firebase-tests
|
||||
variables:
|
||||
TEST_TARGETS: "package me.proton.android.drive.ui.test.flow.photos"
|
||||
|
||||
coverage report:
|
||||
stage: report
|
||||
tags:
|
||||
@@ -404,10 +467,32 @@ publish to firebase app distribution:
|
||||
--release-notes-file "app/src/main/play/release-notes/en-US/default.txt"
|
||||
--groups "qa-team, dev-team, management-team"
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
|
||||
when: manual
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
|
||||
allow_failure: true
|
||||
|
||||
distribute:debug:
|
||||
stage: publish
|
||||
image: $CI_REGISTRY/tpe/test-scripts
|
||||
needs:
|
||||
- job: "build:dev:debug"
|
||||
artifacts: true
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH'
|
||||
allow_failure: true
|
||||
script:
|
||||
- upload_to_nexus.py
|
||||
--path app/build/outputs/apk/dev/debug/ProtonDrive-*-dev-debug.apk
|
||||
--repository "TestData/builds/Drive/Android/LatestDevDebug/"
|
||||
--filename "latest-drive-dev-debug.apk"
|
||||
- upload_to_nexus.py
|
||||
--path app/build/outputs/apk/androidTest/dev/debug/ProtonDrive-*-dev-debug-androidTest.apk
|
||||
--repository "TestData/builds/Drive/Android/LatestTestDevDebug"
|
||||
--filename "latest-drive-test-dev-debug.apk"
|
||||
# Get latest commit information, save it to commit_info.json file and upload to Nexus.
|
||||
- upload_mr_description_to_nexus.py
|
||||
--token $PRIVATE_TOKEN_GITLAB_API_PROTON_CI
|
||||
--repository TestData/builds/Drive/Android/LatestDevDebug
|
||||
|
||||
startReview:
|
||||
needs:
|
||||
- job: "prepare-build"
|
||||
|
||||
@@ -122,6 +122,15 @@ android {
|
||||
}
|
||||
testOptions {
|
||||
execution = "ANDROIDX_TEST_ORCHESTRATOR"
|
||||
managedDevices {
|
||||
localDevices {
|
||||
create("pixel2api30") {
|
||||
device = "Pixel 2"
|
||||
apiLevel = 30
|
||||
systemImageSource = "aosp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val gitHash = "git rev-parse --short HEAD".runCommand(workingDir = rootDir)
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/ProtonTheme.Splash.Drive.StatusBarFix">
|
||||
|
||||
<intent-filter>
|
||||
@@ -66,6 +67,8 @@
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -29,7 +29,9 @@ import me.proton.android.drive.db.DriveDatabase
|
||||
import me.proton.android.drive.photos.data.db.MediaStoreVersionDatabase
|
||||
import me.proton.core.account.data.db.AccountDatabase
|
||||
import me.proton.core.challenge.data.db.ChallengeDatabase
|
||||
import me.proton.core.contact.data.local.db.ContactDatabase
|
||||
import me.proton.core.drive.backup.data.db.BackupDatabase
|
||||
import me.proton.core.drive.base.data.db.BaseDatabase
|
||||
import me.proton.core.drive.device.data.db.DeviceDatabase
|
||||
import me.proton.core.drive.drivelink.data.db.DriveLinkDatabase
|
||||
import me.proton.core.drive.drivelink.download.data.db.DriveLinkDownloadDatabase
|
||||
@@ -55,7 +57,7 @@ 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.stats.data.db.StatsDatabase
|
||||
import me.proton.core.drive.user.data.db.QuotaDatabase
|
||||
import me.proton.core.drive.user.data.db.UserMessageDatabase
|
||||
import me.proton.core.drive.volume.data.db.VolumeDatabase
|
||||
import me.proton.core.drive.worker.data.db.WorkerDatabase
|
||||
import me.proton.core.eventmanager.data.db.EventMetadataDatabase
|
||||
@@ -138,6 +140,9 @@ abstract class DriveDatabaseBindsModule {
|
||||
@Binds
|
||||
abstract fun provideAddressDatabase(db: DriveDatabase): AddressDatabase
|
||||
|
||||
@Binds
|
||||
abstract fun provideContactDatabase(db: DriveDatabase): ContactDatabase
|
||||
|
||||
@Binds
|
||||
abstract fun provideFeatureFlagDatabase(db: DriveDatabase): FeatureFlagDatabase
|
||||
|
||||
@@ -196,7 +201,7 @@ abstract class DriveDatabaseBindsModule {
|
||||
abstract fun provideBackupDatabase(db: DriveDatabase): BackupDatabase
|
||||
|
||||
@Binds
|
||||
abstract fun provideQuotaDatabase(db: DriveDatabase): QuotaDatabase
|
||||
abstract fun provideUserMessageDatabase(db: DriveDatabase): UserMessageDatabase
|
||||
|
||||
@Binds
|
||||
abstract fun provideObservabilityDatabase(db: DriveDatabase): ObservabilityDatabase
|
||||
@@ -233,4 +238,7 @@ abstract class DriveDatabaseBindsModule {
|
||||
|
||||
@Binds
|
||||
abstract fun provideDeviceDatabase(appDatabase: DriveDatabase): DeviceDatabase
|
||||
|
||||
@Binds
|
||||
abstract fun provideBaseDatabase(appDatabase: DriveDatabase): BaseDatabase
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -18,10 +18,18 @@
|
||||
|
||||
package me.proton.android.drive.extension
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.navigation.NavHostController
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
fun NavHostController.runFromRoute(route: String, block: () -> Unit) = takeIf {
|
||||
currentDestination?.route == route
|
||||
}?.let {
|
||||
block()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavHostController.isCurrentDestination(route: String) = currentBackStack.map { entries ->
|
||||
entries.lastOrNull()?.destination?.route == route
|
||||
}.collectAsState(initial = false)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package me.proton.android.drive.extension
|
||||
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
|
||||
internal val ShouldUpgradeStorage.Result.asBoolean: Boolean get() = this != ShouldUpgradeStorage.Result.NoUpgrade
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2021-2023 Proton AG.
|
||||
* Copyright (c) 2021-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -24,7 +24,7 @@ import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import me.proton.core.drive.eventmanager.DriveEventManager
|
||||
import me.proton.core.drive.eventmanager.presentation.DriveEventManager
|
||||
|
||||
@Suppress("unused")
|
||||
class EventManagerInitializer : Initializer<Unit> {
|
||||
|
||||
@@ -52,6 +52,7 @@ class MainInitializer : Initializer<Unit> {
|
||||
BackupInitializer::class.java,
|
||||
TelemetryInitializer::class.java,
|
||||
SelectionInitializer::class.java,
|
||||
PingActiveUserInitializer::class.java,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
+5
-1
@@ -28,6 +28,7 @@ import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.usecase.CancelAllForegroundServices
|
||||
import me.proton.core.account.domain.entity.Account
|
||||
import me.proton.core.accountmanager.domain.AccountManager
|
||||
import me.proton.core.accountmanager.presentation.observe
|
||||
import me.proton.core.accountmanager.presentation.onAccountReady
|
||||
@@ -56,7 +57,7 @@ class NotificationChannelInitializer : Initializer<Unit> {
|
||||
}
|
||||
accountManager.observe(appLifecycleProvider.lifecycle, Lifecycle.State.STARTED)
|
||||
.onAccountReady { account ->
|
||||
createNotificationChannels(account.userId, account.username)
|
||||
createNotificationChannels(account.userId, account.name)
|
||||
}
|
||||
.onAccountRemoved { account ->
|
||||
cancelAllForegroundServices(account.userId)
|
||||
@@ -79,4 +80,7 @@ class NotificationChannelInitializer : Initializer<Unit> {
|
||||
val removeNotificationChannels: RemoveNotificationChannels
|
||||
val cancelAllForegroundServices: CancelAllForegroundServices
|
||||
}
|
||||
|
||||
// We should not have an Account without username yet we'll have a fallback
|
||||
private val Account.name get() = username ?: userId.id.take(6)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import me.proton.android.drive.extension.log
|
||||
import me.proton.core.accountmanager.domain.AccountManager
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.data.domain.usecase.PingActiveUser
|
||||
import me.proton.core.presentation.app.AppLifecycleProvider
|
||||
|
||||
class PingActiveUserInitializer : Initializer<Unit> {
|
||||
override fun create(context: Context) {
|
||||
with (
|
||||
EntryPointAccessors.fromApplication(
|
||||
context.applicationContext,
|
||||
PingActiveUserInitializerEntryPoint::class.java
|
||||
)
|
||||
) {
|
||||
combine(
|
||||
appLifecycleProvider.state,
|
||||
accountManager.getPrimaryUserId(),
|
||||
) { state, primaryUserId ->
|
||||
takeIf { state == AppLifecycleProvider.State.Foreground }
|
||||
?.let {
|
||||
primaryUserId
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach { primaryUserId ->
|
||||
pingActiveUser(primaryUserId)
|
||||
.onFailure { error ->
|
||||
error.log(LogTag.TELEMETRY, "Ping active user failed")
|
||||
}
|
||||
}
|
||||
.launchIn(appLifecycleProvider.lifecycle.coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(
|
||||
LoggerInitializer::class.java,
|
||||
)
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PingActiveUserInitializerEntryPoint {
|
||||
val accountManager: AccountManager
|
||||
val appLifecycleProvider: AppLifecycleProvider
|
||||
val pingActiveUser: PingActiveUser
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -21,6 +21,7 @@ package me.proton.android.drive.settings
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -48,6 +49,7 @@ class DebugSettings(
|
||||
private val prefsAllowBackupDeletedFilesEnabled = booleanPreferencesKey(ALLOW_BACKUP_DELETED_FILES_ENABLED)
|
||||
private val prefsFeatureFlagFreshDuration = longPreferencesKey(FEATURE_FLAG_FRESH_DURATION)
|
||||
private val prefsUseVerifier = booleanPreferencesKey(USE_VERIFIER)
|
||||
private val prefsPhotosUpsellPhotoCount = intPreferencesKey(PHOTOS_UPSELL_PHOTO_COUNT)
|
||||
val baseUrlFlow: Flow<String> = prefsKeyBaseUrl.asFlow(
|
||||
dataStore = context.dataStore,
|
||||
default = buildConfig.baseUrl
|
||||
@@ -130,6 +132,11 @@ class DebugSettings(
|
||||
key = prefsUseVerifier,
|
||||
default = buildConfig.useVerifier,
|
||||
)
|
||||
override var photosUpsellPhotoCount by Delegate(
|
||||
dataStore = context.dataStore,
|
||||
key = prefsPhotosUpsellPhotoCount,
|
||||
default = buildConfig.photosUpsellPhotoCount,
|
||||
)
|
||||
|
||||
fun reset(coroutineScope: CoroutineScope) {
|
||||
coroutineScope.launch {
|
||||
@@ -149,5 +156,6 @@ class DebugSettings(
|
||||
const val ALLOW_BACKUP_DELETED_FILES_ENABLED = "allow_backup_deleted_files_enabled"
|
||||
const val FEATURE_FLAG_FRESH_DURATION = "feature_flag_fresh_duration"
|
||||
const val USE_VERIFIER = "use_verifier"
|
||||
const val PHOTOS_UPSELL_PHOTO_COUNT = "photos_upsell_photo_count"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -73,6 +73,7 @@ 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.navigation.Screen
|
||||
import me.proton.android.drive.ui.provider.LocalSnackbarPadding
|
||||
import me.proton.android.drive.ui.provider.ProvideLocalSnackbarPadding
|
||||
import me.proton.android.drive.ui.viewmodel.AccountViewModel
|
||||
@@ -156,6 +157,11 @@ class MainActivity : FragmentActivity() {
|
||||
deepLinkBaseUrl = this@MainActivity.deepLinkBaseUrl,
|
||||
clearBackstackTrigger = clearBackstackTrigger,
|
||||
deepLinkIntent = deepLinkIntent,
|
||||
defaultStartDestination = if (configurationProvider.photosFeatureFlag) {
|
||||
Screen.Photos.route
|
||||
} else {
|
||||
Screen.Files.route
|
||||
},
|
||||
locked = appLockManager.locked,
|
||||
primaryAccount = accountViewModel.primaryAccount,
|
||||
exitApp = { finish() },
|
||||
@@ -304,7 +310,7 @@ class MainActivity : FragmentActivity() {
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
intent?.let {
|
||||
processIntent(intent, deepLinkIntent, accountViewModel)
|
||||
processIntent(intent, deepLinkIntent, accountViewModel, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.navigationBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ComputerOptionsViewModel
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.device.domain.entity.Device
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
import me.proton.core.drive.drivelink.device.presentation.component.DeviceOptions
|
||||
import me.proton.core.drive.drivelink.device.presentation.options.DeviceOptionEntry
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
|
||||
@Composable
|
||||
fun ComputerOptions(
|
||||
runAction: RunAction,
|
||||
navigateToRenameComputer: (deviceId: DeviceId, folderId: FolderId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<ComputerOptionsViewModel>()
|
||||
val viewModelDevice by rememberFlowWithLifecycle(viewModel.device).collectAsState(initial = null)
|
||||
viewModelDevice?.let { device ->
|
||||
val entries = viewModel.entries(
|
||||
runAction,
|
||||
navigateToRenameComputer,
|
||||
)
|
||||
ComputerOptions(
|
||||
device = device,
|
||||
entries = entries,
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
.testTag(ComputerOptionsTestTag.computerOptions),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ComputerOptions(
|
||||
device: Device,
|
||||
entries: List<DeviceOptionEntry>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
DeviceOptions(
|
||||
device = device,
|
||||
entries = entries,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
object ComputerOptionsTestTag {
|
||||
const val computerOptions = "computer options context menu"
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.effect
|
||||
|
||||
sealed interface PhotosEffect {
|
||||
data object ShowUpsell : PhotosEffect
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -52,6 +52,8 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import me.proton.android.drive.extension.get
|
||||
import me.proton.android.drive.extension.isCurrentDestination
|
||||
import me.proton.android.drive.extension.log
|
||||
import me.proton.android.drive.extension.require
|
||||
import me.proton.android.drive.extension.requireArguments
|
||||
import me.proton.android.drive.extension.requireSerializable
|
||||
@@ -60,6 +62,7 @@ import me.proton.android.drive.lock.presentation.component.AppLock
|
||||
import me.proton.android.drive.log.DriveLogTag
|
||||
import me.proton.android.drive.photos.presentation.component.PhotosPermissionRationale
|
||||
import me.proton.android.drive.ui.dialog.AutoLockDurations
|
||||
import me.proton.android.drive.ui.dialog.ComputerOptions
|
||||
import me.proton.android.drive.ui.dialog.ConfirmDeletionDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmEmptyTrashDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmSkipIssuesDialog
|
||||
@@ -82,11 +85,13 @@ import me.proton.android.drive.ui.options.OptionsFilter
|
||||
import me.proton.android.drive.ui.screen.AppAccessScreen
|
||||
import me.proton.android.drive.ui.screen.BackupIssuesScreen
|
||||
import me.proton.android.drive.ui.screen.FileInfoScreen
|
||||
import me.proton.android.drive.ui.screen.GetMoreFreeStorageScreen
|
||||
import me.proton.android.drive.ui.screen.HomeScreen
|
||||
import me.proton.android.drive.ui.screen.LauncherScreen
|
||||
import me.proton.android.drive.ui.screen.MoveToFolder
|
||||
import me.proton.android.drive.ui.screen.OfflineScreen
|
||||
import me.proton.android.drive.ui.screen.PhotosBackupScreen
|
||||
import me.proton.android.drive.ui.screen.PhotosUpsellScreen
|
||||
import me.proton.android.drive.ui.screen.PreviewScreen
|
||||
import me.proton.android.drive.ui.screen.SettingsScreen
|
||||
import me.proton.android.drive.ui.screen.SigningOutScreen
|
||||
@@ -97,6 +102,8 @@ 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
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
import me.proton.core.drive.drivelink.device.presentation.component.RenameDevice
|
||||
import me.proton.core.drive.drivelink.rename.presentation.Rename
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.DiscardChangesDialog
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.ShareViaLink
|
||||
@@ -117,6 +124,7 @@ fun AppNavGraph(
|
||||
deepLinkBaseUrl: String,
|
||||
clearBackstackTrigger: SharedFlow<Unit>,
|
||||
deepLinkIntent: SharedFlow<Intent>,
|
||||
defaultStartDestination: String,
|
||||
locked: Flow<Boolean>,
|
||||
primaryAccount: Flow<Account?>,
|
||||
exitApp: () -> Unit,
|
||||
@@ -160,8 +168,12 @@ fun AppNavGraph(
|
||||
deepLinkIntent
|
||||
.collectLatest { intent ->
|
||||
CoreLogger.d(DriveLogTag.UI, "Deep link intent received")
|
||||
navController.handleDeepLink(intent)
|
||||
homeNavController = createNavController(localContext)
|
||||
if (!navController.handleDeepLink(intent)) {
|
||||
// clear query params with user information before logging
|
||||
val uri = intent.data?.buildUpon()?.apply { clearQuery() }?.build()
|
||||
IllegalStateException("Invalid deep link: $uri").log(DriveLogTag.UI)
|
||||
homeNavController = createNavController(localContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
AppLock(locked = locked, primaryAccount = primaryAccount) {
|
||||
@@ -169,6 +181,7 @@ fun AppNavGraph(
|
||||
navController = navController,
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
defaultStartDestination = defaultStartDestination,
|
||||
exitApp = exitApp,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
@@ -184,6 +197,7 @@ fun AppNavGraph(
|
||||
navController: NavHostController,
|
||||
homeNavController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
defaultStartDestination: String,
|
||||
exitApp: () -> Unit,
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
@@ -194,13 +208,17 @@ fun AppNavGraph(
|
||||
startDestination = Screen.Launcher.route,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
addLauncher(navController)
|
||||
addLauncher(
|
||||
navController = navController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
)
|
||||
addSignOutConfirmationDialog(navController)
|
||||
addSigningOut()
|
||||
addHome(
|
||||
navController = navController,
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
defaultStartDestination = defaultStartDestination,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
onDrawerStateChanged = onDrawerStateChanged,
|
||||
@@ -238,6 +256,7 @@ fun AppNavGraph(
|
||||
onDrawerStateChanged = onDrawerStateChanged,
|
||||
)
|
||||
addPhotosIssues(navController)
|
||||
addPhotosUpsell(navigateToSubscription)
|
||||
addConfirmSkipIssues(navController)
|
||||
addConfirmStopSyncFolderDialog(navController)
|
||||
addPhotosPermissionRationale(navController)
|
||||
@@ -265,18 +284,35 @@ fun AppNavGraph(
|
||||
addSystemAccessDialog(navController)
|
||||
addAutoLockDurations(navController)
|
||||
addPhotosBackup(navController)
|
||||
addComputerOptions(navController)
|
||||
addRenameComputerDialog(navController)
|
||||
addGetMoreFreeStorage(navController)
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addLauncher(navController: NavHostController) = composable(
|
||||
fun NavGraphBuilder.addLauncher(
|
||||
navController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
) = composable(
|
||||
route = Screen.Launcher.route,
|
||||
) {
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Launcher.REDIRECTION) {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
),
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = Screen.Launcher.deepLink(deepLinkBaseUrl) }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val redirection = navBackStackEntry.get<String>(Screen.Launcher.REDIRECTION)
|
||||
LauncherScreen(
|
||||
foregroundState = navController.isCurrentDestination(route = Screen.Launcher.route),
|
||||
navigateToHomeScreen = { userId ->
|
||||
navController.runFromRoute(route = Screen.Launcher.route) {
|
||||
navController.navigate(Screen.Home(userId)) {
|
||||
navController.navigate(Screen.Home(userId, redirection)) {
|
||||
popUpTo(Screen.Launcher.route) { inclusive = true }
|
||||
}
|
||||
}
|
||||
@@ -544,7 +580,7 @@ internal fun NavGraphBuilder.addHome(
|
||||
homeNavController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
route: String,
|
||||
startDestination: String,
|
||||
defaultStartDestination: String,
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
onDrawerStateChanged: (Boolean) -> Unit,
|
||||
@@ -558,6 +594,13 @@ internal fun NavGraphBuilder.addHome(
|
||||
arguments = arguments,
|
||||
) { navBackStackEntry ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.Files.USER_ID))
|
||||
val startDestination = when (navBackStackEntry.get<String>(Screen.Home.TAB)) {
|
||||
Screen.Home.TAB_FILES -> Screen.Files.route
|
||||
Screen.Home.TAB_PHOTOS -> Screen.Photos.route
|
||||
Screen.Home.TAB_COMPUTERS -> Screen.Computers.route
|
||||
Screen.Home.TAB_SHARED -> Screen.Shared.route
|
||||
else -> defaultStartDestination
|
||||
}
|
||||
val shareId = navBackStackEntry.get<String>(Screen.Files.SHARE_ID)
|
||||
val currentFolderId = navBackStackEntry.get<String>(Screen.Files.FOLDER_ID)?.let { folderId ->
|
||||
shareId?.let {
|
||||
@@ -613,6 +656,11 @@ internal fun NavGraphBuilder.addHome(
|
||||
Screen.BackupIssues.invoke(folderId)
|
||||
)
|
||||
},
|
||||
navigateToPhotosUpsell = {
|
||||
navController.navigate(
|
||||
Screen.Photos.Upsell(userId)
|
||||
)
|
||||
},
|
||||
navigateToBackupSettings = {
|
||||
navController.navigate(
|
||||
Screen.Settings.PhotosBackup(userId)
|
||||
@@ -623,6 +671,16 @@ internal fun NavGraphBuilder.addHome(
|
||||
Screen.PhotosPermissionRationale(userId)
|
||||
)
|
||||
},
|
||||
navigateToComputerOptions = { deviceId ->
|
||||
navController.navigate(
|
||||
Screen.ComputerOptions.invoke(userId, deviceId)
|
||||
)
|
||||
},
|
||||
navigateToGetMoreFreeStorage = {
|
||||
navController.navigate(
|
||||
Screen.GetMoreFreeStorage(userId)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -633,6 +691,7 @@ fun NavGraphBuilder.addHome(
|
||||
navController: NavHostController,
|
||||
homeNavController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
defaultStartDestination: String,
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
onDrawerStateChanged: (Boolean) -> Unit,
|
||||
@@ -641,10 +700,19 @@ fun NavGraphBuilder.addHome(
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
route = Screen.Home.route,
|
||||
startDestination = Screen.Files.route,
|
||||
defaultStartDestination = defaultStartDestination,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
onDrawerStateChanged= onDrawerStateChanged
|
||||
onDrawerStateChanged= onDrawerStateChanged,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Home.USER_ID) {
|
||||
type = NavType.StringType
|
||||
},
|
||||
navArgument(Screen.Home.TAB) {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@@ -661,7 +729,7 @@ fun NavGraphBuilder.addHomeFiles(
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
route = Screen.Files.route,
|
||||
startDestination = Screen.Files.route,
|
||||
defaultStartDestination = Screen.Files.route,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
onDrawerStateChanged= onDrawerStateChanged,
|
||||
@@ -694,7 +762,7 @@ fun NavGraphBuilder.addHomeShared(
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
route = Screen.Shared.route,
|
||||
startDestination = Screen.Shared.route,
|
||||
defaultStartDestination = Screen.Shared.route,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
onDrawerStateChanged = onDrawerStateChanged
|
||||
@@ -714,7 +782,7 @@ fun NavGraphBuilder.addHomePhotos(
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
route = Screen.Photos.route,
|
||||
startDestination = Screen.Photos.route,
|
||||
defaultStartDestination = Screen.Photos.route,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
onDrawerStateChanged = onDrawerStateChanged
|
||||
@@ -734,7 +802,7 @@ fun NavGraphBuilder.addHomeComputers(
|
||||
homeNavController = homeNavController,
|
||||
deepLinkBaseUrl = deepLinkBaseUrl,
|
||||
route = Screen.Computers.route,
|
||||
startDestination = Screen.Computers.route,
|
||||
defaultStartDestination = Screen.Computers.route,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
onDrawerStateChanged = onDrawerStateChanged
|
||||
@@ -1334,3 +1402,79 @@ fun NavGraphBuilder.addPhotosBackup(navController: NavHostController) = composab
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addPhotosUpsell(
|
||||
navigateToSubscription: () -> Unit,
|
||||
) = modalBottomSheet(
|
||||
route = Screen.Photos.Upsell.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Settings.USER_ID) { type = NavType.StringType },
|
||||
),
|
||||
) { _, runAction ->
|
||||
PhotosUpsellScreen(
|
||||
runAction = runAction,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.addComputerOptions(
|
||||
navController: NavHostController,
|
||||
) = modalBottomSheet(
|
||||
route = Screen.ComputerOptions.route,
|
||||
viewState = ModalBottomSheetViewState(dismissOnAction = false),
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Files.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.ComputerOptions.DEVICE_ID) { type = NavType.StringType },
|
||||
),
|
||||
) { navBackStackEntry, runAction ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.Files.USER_ID))
|
||||
ComputerOptions(
|
||||
runAction = runAction,
|
||||
navigateToRenameComputer = { deviceId: DeviceId, folderId: FolderId ->
|
||||
navController.navigate(Screen.Dialogs.RenameComputer.invoke(userId, deviceId, folderId)) {
|
||||
popUpTo(route = Screen.ComputerOptions.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun NavGraphBuilder.addRenameComputerDialog(navController: NavHostController) = dialog(
|
||||
route = Screen.Dialogs.RenameComputer.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Dialogs.RenameComputer.FOLDER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Dialogs.RenameComputer.DEVICE_ID) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
RenameDevice(
|
||||
onDismiss = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Dialogs.RenameComputer.route,
|
||||
inclusive = true,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addGetMoreFreeStorage(navController: NavHostController) = composable(
|
||||
route = Screen.GetMoreFreeStorage.route,
|
||||
enterTransition = defaultEnterSlideTransition(towards = AnimatedContentTransitionScope.SlideDirection.Up) { true },
|
||||
exitTransition = { ExitTransition.None },
|
||||
popEnterTransition = { EnterTransition.None },
|
||||
popExitTransition = defaultPopExitSlideTransition(towards = AnimatedContentTransitionScope.SlideDirection.Down) { true },
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Settings.USER_ID) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
GetMoreFreeStorageScreen(
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.GetMoreFreeStorage.route,
|
||||
inclusive = true,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -30,7 +30,6 @@ import androidx.navigation.navArgument
|
||||
import androidx.navigation.navDeepLink
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import me.proton.android.drive.extension.get
|
||||
import me.proton.android.drive.extension.require
|
||||
import me.proton.android.drive.ui.navigation.animation.defaultEnterSlideTransition
|
||||
import me.proton.android.drive.ui.navigation.animation.defaultExitSlideTransition
|
||||
import me.proton.android.drive.ui.navigation.animation.defaultPopEnterSlideTransition
|
||||
@@ -44,6 +43,7 @@ import me.proton.android.drive.ui.screen.SharedScreen
|
||||
import me.proton.android.drive.ui.screen.SyncedFoldersScreen
|
||||
import me.proton.android.drive.ui.viewstate.HomeScaffoldState
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
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
|
||||
@@ -68,7 +68,9 @@ fun HomeNavGraph(
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
) = DriveNavHost(
|
||||
navController = homeNavController,
|
||||
startDestination = startDestination
|
||||
@@ -85,13 +87,18 @@ fun HomeNavGraph(
|
||||
navigateToParentFolderOptions,
|
||||
)
|
||||
addShared(
|
||||
homeScaffoldState,
|
||||
homeNavController,
|
||||
deepLinkBaseUrl,
|
||||
arguments,
|
||||
homeScaffoldState,
|
||||
{ fileId -> navigateToPreview(fileId, PagerType.SINGLE, OptionsFilter.FILES) },
|
||||
navigateToSorting,
|
||||
{ linkId -> navigateToFileOrFolderOptions(linkId, OptionsFilter.FILES) },
|
||||
)
|
||||
addPhotos(
|
||||
homeNavController,
|
||||
deepLinkBaseUrl,
|
||||
arguments,
|
||||
homeScaffoldState,
|
||||
navigateToPhotosPermissionRationale,
|
||||
navigateToPhotosPreview = { fileId -> navigateToPreview(fileId, PagerType.PHOTO, OptionsFilter.PHOTOS) },
|
||||
@@ -101,16 +108,20 @@ fun HomeNavGraph(
|
||||
},
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
)
|
||||
addComputers(
|
||||
homeScaffoldState,
|
||||
homeNavController,
|
||||
deepLinkBaseUrl,
|
||||
arguments,
|
||||
homeScaffoldState,
|
||||
{ fileId -> navigateToPreview(fileId, PagerType.FOLDER, OptionsFilter.FILES) },
|
||||
navigateToSorting,
|
||||
{ linkId -> navigateToFileOrFolderOptions(linkId, OptionsFilter.FILES) },
|
||||
{ selectionId -> navigateToMultipleFileOrFolderOptions(selectionId, OptionsFilter.FILES) },
|
||||
navigateToParentFolderOptions,
|
||||
navigateToComputerOptions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,13 +197,14 @@ fun NavGraphBuilder.addFiles(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addShared(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
arguments: Bundle,
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPreview: (linkId: FileId) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
@@ -205,23 +217,40 @@ fun NavGraphBuilder.addShared(
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
),
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = Screen.Shared.deepLink(deepLinkBaseUrl) }
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.Files.USER_ID))
|
||||
SharedScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = { folderId, folderName ->
|
||||
navController.navigate(Screen.Files(userId, folderId, folderName))
|
||||
},
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToSortingDialog = navigateToSorting,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
)
|
||||
navBackStackEntry.get<String>(Screen.Shared.USER_ID)?.let { userId ->
|
||||
SharedScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = { folderId, folderName ->
|
||||
navController.navigate(Screen.Files(UserId(userId), folderId, folderName))
|
||||
},
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToSortingDialog = navigateToSorting,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
)
|
||||
} ?: let {
|
||||
val userId = UserId(requireNotNull(arguments.getString(Screen.Shared.USER_ID)))
|
||||
val shareId = arguments.getString(Screen.Shared.SHARE_ID)?.let { shareId ->
|
||||
ShareId(userId, shareId)
|
||||
}
|
||||
navController.navigate(Screen.Shared(userId, shareId)) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addPhotos(
|
||||
navController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
arguments: Bundle,
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId) -> Unit,
|
||||
@@ -229,6 +258,7 @@ fun NavGraphBuilder.addPhotos(
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
) = composable(
|
||||
route = Screen.Photos.route,
|
||||
@@ -239,30 +269,49 @@ fun NavGraphBuilder.addPhotos(
|
||||
nullable = true
|
||||
defaultValue = null
|
||||
},
|
||||
),
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = Screen.Photos.deepLink(deepLinkBaseUrl) }
|
||||
)
|
||||
) {
|
||||
PhotosScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToPhotosPermissionRationale = navigateToPhotosPermissionRationale,
|
||||
navigateToPhotosPreview = navigateToPhotosPreview,
|
||||
navigateToPhotosOptions = navigateToPhotosOptions,
|
||||
navigateToMultiplePhotosOptions = navigateToMultiplePhotosOptions,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
)
|
||||
) { navBackStackEntry ->
|
||||
navBackStackEntry.get<String>(Screen.Photos.USER_ID)?.let { _ ->
|
||||
PhotosScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToPhotosPermissionRationale = navigateToPhotosPermissionRationale,
|
||||
navigateToPhotosPreview = navigateToPhotosPreview,
|
||||
navigateToPhotosOptions = navigateToPhotosOptions,
|
||||
navigateToMultiplePhotosOptions = navigateToMultiplePhotosOptions,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
)
|
||||
} ?: let {
|
||||
val userId = UserId(requireNotNull(arguments.getString(Screen.Photos.USER_ID)))
|
||||
val shareId = arguments.getString(Screen.Photos.SHARE_ID)?.let { shareId ->
|
||||
ShareId(userId, shareId)
|
||||
}
|
||||
navController.navigate(Screen.Photos(userId, shareId)) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addComputers(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navController: NavHostController,
|
||||
deepLinkBaseUrl: String,
|
||||
arguments: Bundle,
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPreview: (linkId: FileId) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
) = composable(
|
||||
route = Screen.Computers.route,
|
||||
enterTransition = defaultEnterSlideTransition {
|
||||
@@ -301,47 +350,87 @@ fun NavGraphBuilder.addComputers(
|
||||
nullable = false
|
||||
defaultValue = false
|
||||
}
|
||||
)
|
||||
),
|
||||
deepLinks = listOf(
|
||||
navDeepLink { uriPattern = Screen.Computers.deepLink(deepLinkBaseUrl) }
|
||||
),
|
||||
) { navBackStackEntry ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.Computers.USER_ID))
|
||||
val argShareId = navBackStackEntry.get<String>(Screen.Files.SHARE_ID)
|
||||
val argFolderId = navBackStackEntry.get<String>(Screen.Files.FOLDER_ID)
|
||||
val argSyncedFolders = navBackStackEntry.arguments?.getBoolean(Screen.Computers.SYNCED_FOLDERS, false) ?: false
|
||||
if (argShareId != null && argFolderId != null) {
|
||||
if (argSyncedFolders) {
|
||||
SyncedFoldersScreen(
|
||||
navBackStackEntry.get<String>(Screen.Computers.USER_ID)?.let { userId ->
|
||||
val argShareId = navBackStackEntry.get<String>(Screen.Files.SHARE_ID)
|
||||
val argFolderId = navBackStackEntry.get<String>(Screen.Files.FOLDER_ID)
|
||||
val argSyncedFolders =
|
||||
navBackStackEntry.arguments?.getBoolean(Screen.Computers.SYNCED_FOLDERS, false) ?: false
|
||||
if (argShareId != null && argFolderId != null) {
|
||||
if (argSyncedFolders) {
|
||||
SyncedFoldersScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = { folderId, folderName ->
|
||||
navController.navigate(
|
||||
Screen.Computers(
|
||||
userId = UserId(userId),
|
||||
folderId = folderId,
|
||||
folderName = folderName,
|
||||
syncedFolders = false
|
||||
)
|
||||
)
|
||||
},
|
||||
navigateToSortingDialog = navigateToSorting,
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Computers.route,
|
||||
inclusive = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
FilesScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = { folderId, folderName ->
|
||||
navController.navigate(
|
||||
Screen.Computers(
|
||||
UserId(userId),
|
||||
folderId,
|
||||
folderName,
|
||||
false
|
||||
)
|
||||
)
|
||||
},
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToSortingDialog = navigateToSorting,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToMultipleFileOrFolderOptions = navigateToMultipleFileOrFolderOptions,
|
||||
navigateToParentFolderOptions = navigateToParentFolderOptions,
|
||||
navigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ComputersScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = { folderId, folderName ->
|
||||
navController.navigate(Screen.Computers(userId, folderId, folderName, false))
|
||||
},
|
||||
navigateToSortingDialog = navigateToSorting,
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Computers.route,
|
||||
inclusive = true,
|
||||
navigateToSyncedFolders = { folderId, folderName ->
|
||||
navController.navigate(
|
||||
Screen.Computers(
|
||||
UserId(userId),
|
||||
folderId,
|
||||
folderName,
|
||||
true
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
FilesScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = { folderId, folderName ->
|
||||
navController.navigate(Screen.Computers(userId, folderId, folderName, false))
|
||||
},
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToSortingDialog = navigateToSorting,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToMultipleFileOrFolderOptions = navigateToMultipleFileOrFolderOptions,
|
||||
navigateToParentFolderOptions = navigateToParentFolderOptions,
|
||||
navigateBack = { navController.popBackStack() },
|
||||
navigateToComputerOptions = navigateToComputerOptions,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ComputersScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToSyncedFolders = { folderId, folderName ->
|
||||
navController.navigate(Screen.Computers(userId, folderId, folderName, true))
|
||||
},
|
||||
)
|
||||
} ?: let {
|
||||
val userId = UserId(requireNotNull(arguments.getString(Screen.Files.USER_ID)))
|
||||
val folderId = arguments.getString(Screen.Files.SHARE_ID)?.let { shareId ->
|
||||
arguments.getString(Screen.Files.FOLDER_ID)?.let { folderId ->
|
||||
FolderId(ShareId(userId, shareId), folderId)
|
||||
}
|
||||
}
|
||||
val folderName = arguments.getString(Screen.Files.FOLDER_NAME)
|
||||
navController.navigate(Screen.Computers(userId, folderId, folderName, false)) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -27,6 +27,7 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import me.proton.android.drive.ui.options.OptionsFilter
|
||||
import me.proton.android.drive.ui.viewmodel.ComputerOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.FileOrFolderOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.MoveToFolderViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.MultipleFileOrFolderOptionsViewModel
|
||||
@@ -34,7 +35,9 @@ import me.proton.android.drive.ui.viewmodel.ParentFolderOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.UploadToViewModel
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.drivelink.rename.presentation.RenameViewModel
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
import me.proton.core.drive.drivelink.device.presentation.viewmodel.RenameDeviceViewModel
|
||||
import me.proton.core.drive.drivelink.rename.presentation.viewmodel.RenameViewModel
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewmodel.SharedDriveLinkViewModel
|
||||
import me.proton.core.drive.folder.create.presentation.CreateFolderViewModel
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
@@ -48,7 +51,9 @@ import me.proton.core.drive.sorting.domain.entity.Direction
|
||||
sealed class Screen(val route: String) {
|
||||
open fun deepLink(baseUrl: String): String? = "$baseUrl/$route"
|
||||
|
||||
data object Launcher : Screen("launcher")
|
||||
data object Launcher : Screen("launcher?redirection={redirection}") {
|
||||
const val REDIRECTION = "redirection"
|
||||
}
|
||||
|
||||
data object SigningOut : Screen("signingOut/{userId}") {
|
||||
operator fun invoke(userId: UserId) = "signingOut/${userId.id}"
|
||||
@@ -56,10 +61,20 @@ sealed class Screen(val route: String) {
|
||||
const val USER_ID = Screen.USER_ID
|
||||
}
|
||||
|
||||
data object Home : Screen("home/{userId}") {
|
||||
operator fun invoke(userId: UserId) = "home/${userId.id}"
|
||||
data object Home : Screen("home/{userId}?tab={tab}") {
|
||||
operator fun invoke(userId: UserId, tab: String? = null) = buildString {
|
||||
append("home/${userId.id}")
|
||||
if (tab != null) {
|
||||
append("?tab=$tab")
|
||||
}
|
||||
}
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val TAB = "tab"
|
||||
const val TAB_FILES = "files"
|
||||
const val TAB_PHOTOS = "photos"
|
||||
const val TAB_COMPUTERS = "computers"
|
||||
const val TAB_SHARED = "shared"
|
||||
}
|
||||
|
||||
data object Sorting :
|
||||
@@ -123,6 +138,18 @@ sealed class Screen(val route: String) {
|
||||
const val SHARE_ID = ParentFolderOptionsViewModel.KEY_SHARE_ID
|
||||
}
|
||||
|
||||
data object ComputerOptions : Screen(
|
||||
"options/computer/{userId}/devices/{deviceId}"
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
deviceId: DeviceId,
|
||||
) = "options/computer/${userId.id}/devices/${deviceId.id}"
|
||||
|
||||
const val DEVICE_ID = ComputerOptionsViewModel.KEY_DEVICE_ID
|
||||
}
|
||||
|
||||
data object Info : Screen("info/{userId}/shares/{shareId}/files?linkId={linkId}") {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
@@ -252,6 +279,10 @@ sealed class Screen(val route: String) {
|
||||
|
||||
operator fun invoke(userId: UserId, shareId: ShareId?) = "home/${userId.id}/photos/${shareId?.id}"
|
||||
|
||||
object Upsell : Screen("home/{userId}/photos/upsell"){
|
||||
operator fun invoke(userId: UserId) = "home/${userId.id}/photos/upsell"
|
||||
}
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val SHARE_ID = "shareId"
|
||||
}
|
||||
@@ -291,8 +322,9 @@ sealed class Screen(val route: String) {
|
||||
override fun invoke(userId: UserId) =
|
||||
filesBrowsableBuildRoute("computers", userId, null, null)
|
||||
|
||||
operator fun invoke(userId: UserId, folderId: FolderId, folderName: String?, syncedFolders: Boolean) =
|
||||
filesBrowsableBuildRoute("computers", userId, folderId, folderName) + "&syncedFolders=${syncedFolders}"
|
||||
operator fun invoke(userId: UserId, folderId: FolderId?, folderName: String?, syncedFolders: Boolean) =
|
||||
filesBrowsableBuildRoute("computers", userId, folderId, folderName) +
|
||||
folderId?.let{"&syncedFolders=${syncedFolders}"}.orEmpty()
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val SYNCED_FOLDERS = "syncedFolders"
|
||||
@@ -421,6 +453,28 @@ sealed class Screen(val route: String) {
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
}
|
||||
|
||||
data object RenameComputer : Screen(
|
||||
"rename/{userId}/shares/{shareId}/files?fileId={fileId}&folderId={folderId}&deviceId={deviceId}"
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
deviceId: DeviceId,
|
||||
folderId: FolderId,
|
||||
) = "rename/${userId.id}/shares/${folderId.shareId.id}/files?folderId=${folderId.id}&deviceId=${deviceId.id}"
|
||||
|
||||
const val FOLDER_ID = RenameViewModel.KEY_FOLDER_ID
|
||||
const val SHARE_ID = RenameViewModel.KEY_SHARE_ID
|
||||
const val DEVICE_ID = RenameDeviceViewModel.KEY_DEVICE_ID
|
||||
}
|
||||
}
|
||||
|
||||
data object GetMoreFreeStorage : Screen("storage/{userId}/getMoreFree") {
|
||||
|
||||
operator fun invoke(userId: UserId) = "storage/${userId.id}/getMoreFree"
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -29,6 +29,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.proton.android.drive.ui.effect.HandleHomeEffect
|
||||
@@ -40,6 +41,7 @@ import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.drive.base.presentation.component.ProtonPullToRefresh
|
||||
import me.proton.core.drive.base.presentation.extension.conditional
|
||||
import me.proton.core.drive.device.domain.entity.Device
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
import me.proton.core.drive.drivelink.device.presentation.component.DevicesContent
|
||||
import me.proton.core.drive.drivelink.device.presentation.component.DevicesEmpty
|
||||
import me.proton.core.drive.drivelink.device.presentation.component.DevicesError
|
||||
@@ -56,13 +58,17 @@ import me.proton.core.drive.base.presentation.component.TopAppBar as BaseTopAppB
|
||||
fun ComputersScreen(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToSyncedFolders: (FolderId, String?) -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<ComputersViewModel>()
|
||||
val viewState by rememberFlowWithLifecycle(flow = viewModel.viewState)
|
||||
.collectAsState(initial = viewModel.initialViewState)
|
||||
val viewEvent = remember {
|
||||
viewModel.viewEvent(navigateToSyncedFolders)
|
||||
viewModel.viewEvent(
|
||||
navigateToSyncedFolders,
|
||||
navigateToComputerOptions,
|
||||
)
|
||||
}
|
||||
val devices by rememberFlowWithLifecycle(viewModel.devices).collectAsState(initial = null)
|
||||
viewModel.HandleHomeEffect(homeScaffoldState)
|
||||
@@ -101,9 +107,13 @@ fun Computers(
|
||||
devices = devices,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
onError = {},
|
||||
) { device ->
|
||||
viewEvent.onDevice(device)
|
||||
}
|
||||
onDevice = { device ->
|
||||
viewEvent.onDevice(device)
|
||||
},
|
||||
onMoreOptions = { device ->
|
||||
viewEvent.onMoreOptions(device)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +124,7 @@ fun Computers(
|
||||
modifier: Modifier = Modifier,
|
||||
onError: () -> Unit,
|
||||
onDevice: (Device) -> Unit,
|
||||
onMoreOptions: (Device) -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
Box(
|
||||
@@ -146,6 +157,9 @@ fun Computers(
|
||||
DevicesContent(
|
||||
devices = devices,
|
||||
onClick = onDevice,
|
||||
onMoreOptions = onMoreOptions,
|
||||
modifier = Modifier
|
||||
.testTag(ComputersTestTag.content)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -160,7 +174,12 @@ private fun TopAppBar(
|
||||
navigationIcon = if (viewState.navigationIconResId != 0) {
|
||||
painterResource(id = viewState.navigationIconResId)
|
||||
} else null,
|
||||
notificationDotVisible = viewState.notificationDotVisible,
|
||||
onNavigationIcon = viewEvent.onTopAppBarNavigation,
|
||||
title = viewState.title,
|
||||
)
|
||||
}
|
||||
|
||||
object ComputersTestTag {
|
||||
const val content = "computers content"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.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.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.GetMoreFreeStorageViewModel
|
||||
import me.proton.android.drive.ui.viewstate.GetMoreFreeStorageViewState
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonDimens.DefaultSpacing
|
||||
import me.proton.core.compose.theme.ProtonDimens.ExtraSmallSpacing
|
||||
import me.proton.core.compose.theme.ProtonDimens.MediumSpacing
|
||||
import me.proton.core.compose.theme.ProtonDimens.SmallSpacing
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.defaultHighlightNorm
|
||||
import me.proton.core.compose.theme.defaultSmallNorm
|
||||
import me.proton.core.compose.theme.defaultWeak
|
||||
import me.proton.core.compose.theme.headlineNorm
|
||||
import me.proton.core.drive.base.domain.extension.GiB
|
||||
import me.proton.core.drive.base.presentation.component.TopAppBar
|
||||
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
|
||||
import me.proton.core.drive.base.presentation.extension.isLandscape
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@Composable
|
||||
fun GetMoreFreeStorageScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<GetMoreFreeStorageViewModel>()
|
||||
val viewState by rememberFlowWithLifecycle(viewModel.viewState).collectAsState(
|
||||
initial = viewModel.initialViewState
|
||||
)
|
||||
GetMoreFreeStorage(
|
||||
viewState = viewState,
|
||||
navigateBack = navigateBack,
|
||||
modifier = modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GetMoreFreeStorage(
|
||||
viewState: GetMoreFreeStorageViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
.fillMaxSize()
|
||||
.testTag(GetMoreFreeStorage.screen),
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
TopAppBar(navigateBack = navigateBack)
|
||||
Content(
|
||||
viewState = viewState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopAppBar(
|
||||
modifier: Modifier = Modifier,
|
||||
navigateBack: () -> Unit,
|
||||
) {
|
||||
TopAppBar(
|
||||
navigationIcon = painterResource(id = CorePresentation.drawable.ic_proton_close),
|
||||
onNavigationIcon = navigateBack,
|
||||
title = {},
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(
|
||||
viewState: GetMoreFreeStorageViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (isLandscape) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
.padding(DefaultSpacing),
|
||||
) {
|
||||
TitlePart(
|
||||
viewState = viewState,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = SmallSpacing)
|
||||
.weight(1f),
|
||||
)
|
||||
ActionsPart(
|
||||
actions = viewState.actions,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = SmallSpacing)
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
.padding(DefaultSpacing),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TitlePart(
|
||||
viewState = viewState,
|
||||
)
|
||||
ActionsPart(
|
||||
actions = viewState.actions,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitlePart(
|
||||
viewState: GetMoreFreeStorageViewState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(painter = painterResource(id = viewState.imageResId), contentDescription = null)
|
||||
Text(
|
||||
text = viewState.title,
|
||||
style = ProtonTheme.typography.headlineNorm,
|
||||
modifier = Modifier.padding(top = MediumSpacing)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = viewState.descriptionResId),
|
||||
style = ProtonTheme.typography.defaultWeak,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = SmallSpacing, bottom = DefaultSpacing)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionsPart(
|
||||
actions: List<GetMoreFreeStorageViewState.Action>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
actions.forEach { action ->
|
||||
ListItem(action = action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ListItem(
|
||||
action: GetMoreFreeStorageViewState.Action,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = SmallSpacing),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
ActionIcon(
|
||||
iconResId = action.iconResId,
|
||||
iconTintColor = if (action.isDone) ProtonTheme.colors.notificationSuccess else ProtonTheme.colors.iconAccent,
|
||||
bgColor = if (action.isDone) ProtonTheme.colors.notificationSuccess.copy(alpha = 0.06F) else ProtonTheme.colors.backgroundSecondary
|
||||
)
|
||||
ActionDetails(
|
||||
titleResId = action.titleResId,
|
||||
titleTextStyle = ProtonTheme.typography.defaultHighlightNorm.copy(
|
||||
color = if (action.isDone) ProtonTheme.colors.textWeak else ProtonTheme.colors.textNorm
|
||||
),
|
||||
subtitle = action.getDescription(),
|
||||
subtitleTextStyle = ProtonTheme.typography.defaultSmallNorm,
|
||||
onSubtitleClick = action.onSubtitleClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionIcon(
|
||||
iconResId: Int,
|
||||
iconTintColor: Color,
|
||||
bgColor: Color,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.sizeIn(minHeight = 40.dp, minWidth = 40.dp)
|
||||
.background(
|
||||
color = bgColor,
|
||||
shape = RoundedCornerShape(ProtonDimens.ExtraLargeCornerRadius),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconResId),
|
||||
tint = iconTintColor,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionDetails(
|
||||
titleResId: Int,
|
||||
titleTextStyle: TextStyle,
|
||||
subtitle: AnnotatedString,
|
||||
subtitleTextStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
onSubtitleClick: (Int) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.sizeIn(minHeight = 64.dp)
|
||||
.padding(start = DefaultSpacing),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = titleResId),
|
||||
style = titleTextStyle,
|
||||
)
|
||||
ClickableText(
|
||||
text = subtitle,
|
||||
style = subtitleTextStyle,
|
||||
onClick = onSubtitleClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewGetMoreFreeStorage() {
|
||||
ProtonTheme {
|
||||
GetMoreFreeStorage(
|
||||
viewState = GetMoreFreeStorageViewState(
|
||||
imageResId = BasePresentation.drawable.img_free_storage,
|
||||
title = stringResource(
|
||||
id = I18N.string.get_more_free_storage_title,
|
||||
5.GiB.asHumanReadableString(LocalContext.current, numberOfDecimals = 0),
|
||||
),
|
||||
descriptionResId = I18N.string.get_more_free_storage_description,
|
||||
actions = listOf(
|
||||
GetMoreFreeStorageViewState.Action(
|
||||
iconResId = CorePresentation.drawable.ic_proton_arrow_up_line,
|
||||
titleResId = I18N.string.get_more_free_storage_action_upload_title,
|
||||
getDescription = {
|
||||
AnnotatedString(
|
||||
stringResource(id = I18N.string.get_more_free_storage_action_upload_subtitle)
|
||||
)
|
||||
},
|
||||
isDone = false,
|
||||
),
|
||||
GetMoreFreeStorageViewState.Action(
|
||||
iconResId = CorePresentation.drawable.ic_proton_checkmark,
|
||||
titleResId = I18N.string.get_more_free_storage_action_link_title,
|
||||
getDescription = {
|
||||
AnnotatedString(
|
||||
"Select any file of folder. Open the options menu and press Get link."
|
||||
)
|
||||
},
|
||||
isDone = true,
|
||||
),
|
||||
GetMoreFreeStorageViewState.Action(
|
||||
iconResId = CorePresentation.drawable.ic_proton_key,
|
||||
titleResId = I18N.string.get_more_free_storage_action_recovery_title,
|
||||
getDescription = {
|
||||
AnnotatedString(
|
||||
text = "Sign in at account.proton.me, then go to Settings -> Recovery."
|
||||
)
|
||||
},
|
||||
isDone = false,
|
||||
),
|
||||
),
|
||||
),
|
||||
navigateBack = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object GetMoreFreeStorage {
|
||||
const val screen = "GetMoreFreeStorage screen"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -64,6 +64,7 @@ import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.presentation.component.BottomNavigation
|
||||
import me.proton.core.drive.base.presentation.component.ModalBottomSheet
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
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
|
||||
@@ -93,8 +94,11 @@ fun HomeScreen(
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
navigateToGetMoreFreeStorage: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
setLocalSnackbarPadding(BottomNavigationHeight)
|
||||
@@ -125,7 +129,9 @@ fun HomeScreen(
|
||||
navigateToPhotosPermissionRationale = navigateToPhotosPermissionRationale,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToComputerOptions = navigateToComputerOptions,
|
||||
arguments = arguments,
|
||||
viewState = currentViewState,
|
||||
viewEvent = homeViewModel.viewEvent(
|
||||
@@ -133,7 +139,7 @@ fun HomeScreen(
|
||||
navigateToTrash = navigateToTrash,
|
||||
navigateToTab = { route ->
|
||||
homeNavController.navigate(route) {
|
||||
popUpTo(Screen.Files.route) { inclusive = route == Screen.Files(userId) }
|
||||
popUpTo(Screen.Photos.route) { inclusive = route == Screen.Photos(userId) }
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
@@ -141,6 +147,7 @@ fun HomeScreen(
|
||||
navigateToSettings = navigateToSettings,
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToGetMoreFreeStorage = navigateToGetMoreFreeStorage,
|
||||
),
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
@@ -170,7 +177,9 @@ internal fun Home(
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
) {
|
||||
val homeScaffoldState = rememberHomeScaffoldState()
|
||||
val isDrawerOpen = with(homeScaffoldState.scaffoldState.drawerState) {
|
||||
@@ -262,7 +271,9 @@ internal fun Home(
|
||||
navigateToPhotosPermissionRationale,
|
||||
navigateToSubscription,
|
||||
navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell,
|
||||
navigateToBackupSettings,
|
||||
navigateToComputerOptions,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -22,8 +22,10 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -37,38 +39,52 @@ import me.proton.core.domain.entity.UserId
|
||||
@Composable
|
||||
@ExperimentalCoroutinesApi
|
||||
fun LauncherScreen(
|
||||
foregroundState: State<Boolean>,
|
||||
navigateToHomeScreen: (userId: UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val launcherViewModel = hiltViewModel<LauncherViewModel>()
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize()
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Launcher(launcherViewModel, navigateToHomeScreen)
|
||||
Launcher(
|
||||
foregroundState = foregroundState,
|
||||
viewModel = launcherViewModel,
|
||||
navigateToHomeScreen = navigateToHomeScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ExperimentalCoroutinesApi
|
||||
internal fun Launcher(
|
||||
foregroundState: State<Boolean>,
|
||||
viewModel: LauncherViewModel,
|
||||
navigateToHomeScreen: (userId: UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewState by rememberFlowWithLifecycle(viewModel.viewState)
|
||||
.collectAsState(initial = LauncherViewState.initialValue)
|
||||
Launcher(viewState, navigateToHomeScreen, modifier)
|
||||
val foreground by foregroundState
|
||||
Launcher(
|
||||
foreground = foreground,
|
||||
viewState = viewState,
|
||||
navigateToHomeScreen = navigateToHomeScreen,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun Launcher(
|
||||
foreground: Boolean,
|
||||
viewState: LauncherViewState,
|
||||
navigateToHomeScreen: (userId: UserId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val state = viewState.primaryAccountState
|
||||
LaunchedEffect(state) {
|
||||
if (state is PrimaryAccountState.SignedIn) {
|
||||
LaunchedEffect(state, foreground) {
|
||||
if (state is PrimaryAccountState.SignedIn && foreground) {
|
||||
navigateToHomeScreen(state.userId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +146,9 @@ fun MoveToFolder(
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.padding(start = SmallSpacing),
|
||||
modifier = Modifier
|
||||
.padding(start = SmallSpacing)
|
||||
.testTag(MoveToFolderScreenTestTag.moveButton),
|
||||
enabled = viewState.isMoveButtonEnabled,
|
||||
onClick = viewEvent.move,
|
||||
) {
|
||||
@@ -193,4 +195,5 @@ fun Title(
|
||||
object MoveToFolderScreenTestTag {
|
||||
const val screen = "move to folder screen"
|
||||
const val plusFolderButton = "move to folder plus folder button"
|
||||
const val moveButton = "move button"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -31,10 +31,15 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.paging.PagingData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.photos.presentation.component.BackupPermissions
|
||||
import me.proton.android.drive.photos.presentation.component.Photos
|
||||
import me.proton.android.drive.photos.presentation.component.PhotosStatesIndicator
|
||||
@@ -42,6 +47,7 @@ import me.proton.android.drive.photos.presentation.state.PhotosItem
|
||||
import me.proton.android.drive.photos.presentation.viewevent.PhotosViewEvent
|
||||
import me.proton.android.drive.photos.presentation.viewstate.PhotosViewState
|
||||
import me.proton.android.drive.ui.effect.HandleHomeEffect
|
||||
import me.proton.android.drive.ui.effect.PhotosEffect
|
||||
import me.proton.android.drive.ui.viewmodel.PhotosViewModel
|
||||
import me.proton.android.drive.ui.viewstate.HomeScaffoldState
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
@@ -64,6 +70,7 @@ fun PhotosScreen(
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PhotosViewModel>()
|
||||
@@ -76,12 +83,23 @@ fun PhotosScreen(
|
||||
navigateToMultiplePhotosOptions = navigateToMultiplePhotosOptions,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
)
|
||||
}
|
||||
val photos = rememberFlowWithLifecycle(flow = viewModel.driveLinks)
|
||||
val listEffect = rememberFlowWithLifecycle(flow = viewModel.listEffect)
|
||||
|
||||
LaunchedEffect(viewModel, LocalContext.current) {
|
||||
viewModel.photosEffect.onEach { effect ->
|
||||
when (effect) {
|
||||
PhotosEffect.ShowUpsell -> launch(Dispatchers.Main) {
|
||||
viewEvent.onShowUpsell()
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
viewModel.HandleHomeEffect(homeScaffoldState)
|
||||
|
||||
PhotosScreen(
|
||||
@@ -161,6 +179,7 @@ fun TopAppBar(
|
||||
navigationIcon = if (viewState.navigationIconResId != 0) {
|
||||
painterResource(id = viewState.navigationIconResId)
|
||||
} else null,
|
||||
notificationDotVisible = viewState.notificationDotVisible,
|
||||
onNavigationIcon = viewEvent.onTopAppBarNavigation,
|
||||
title = viewState.title,
|
||||
actions = actions
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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 me.proton.android.drive.photos.presentation.viewevent.PhotosUpsellViewEvent
|
||||
import me.proton.android.drive.ui.viewmodel.PhotosUpsellViewModel
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
|
||||
import me.proton.core.drive.base.presentation.component.IllustratedMessage
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
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.R as BasePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@Composable
|
||||
fun PhotosUpsellScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
runAction: RunAction,
|
||||
navigateToSubscription: () -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PhotosUpsellViewModel>()
|
||||
val viewEvent = remember {
|
||||
viewModel.viewEvent(
|
||||
runAction = runAction,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit){
|
||||
onDispose {
|
||||
viewEvent.onDismiss()
|
||||
}
|
||||
}
|
||||
PhotosUpsell(
|
||||
viewEvent = viewEvent,
|
||||
modifier = modifier
|
||||
.systemBarsPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotosUpsell(
|
||||
viewEvent: PhotosUpsellViewEvent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = ProtonDimens.DefaultSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(ProtonDimens.DefaultSpacing),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
IllustratedMessage(
|
||||
imageResId = getThemeDrawableId(
|
||||
light = BasePresentation.drawable.img_upsell_drive_light,
|
||||
dark = BasePresentation.drawable.img_upsell_drive_dark,
|
||||
dayNight = BasePresentation.drawable.img_upsell_drive_daynight,
|
||||
),
|
||||
titleResId = I18N.string.photos_upsell_title,
|
||||
descriptionResId = I18N.string.photos_upsell_description,
|
||||
)
|
||||
val buttonModifier = Modifier
|
||||
.conditional(isPortrait) {
|
||||
fillMaxWidth()
|
||||
}
|
||||
.conditional(isLandscape) {
|
||||
widthIn(min = ButtonMinWidth)
|
||||
}
|
||||
.heightIn(min = ProtonDimens.ListItemHeight)
|
||||
ProtonSolidButton(
|
||||
onClick = { viewEvent.onMoreStorage() },
|
||||
modifier = buttonModifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = I18N.string.photos_upsell_get_storage_action),
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
ProtonTextButton(
|
||||
onClick = { viewEvent.onCancel() },
|
||||
modifier = buttonModifier
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = I18N.string.photos_upsell_dismiss_action),
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ButtonMinWidth = 300.dp
|
||||
|
||||
@Preview
|
||||
@Preview(widthDp = 600, heightDp = 360)
|
||||
@Composable
|
||||
private fun PhotosUpsellPreview() {
|
||||
ProtonTheme {
|
||||
PhotosUpsell(
|
||||
viewEvent = object : PhotosUpsellViewEvent {},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.proton.android.drive.ui.effect.HandleHomeEffect
|
||||
@@ -62,7 +63,9 @@ fun SyncedFoldersScreen(
|
||||
driveLinks = PagingList(viewModel.driveLinks, viewModel.listEffect),
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.testTag(SyncedFoldersTestTag.screen),
|
||||
onRefresh = viewModel::refresh,
|
||||
)
|
||||
}
|
||||
@@ -114,3 +117,7 @@ private fun TopAppBar(
|
||||
isTitleEncrypted = viewState.isTitleEncrypted,
|
||||
)
|
||||
}
|
||||
|
||||
object SyncedFoldersTestTag {
|
||||
const val screen = "computer synced folders screen"
|
||||
}
|
||||
|
||||
@@ -24,4 +24,5 @@ interface ComputersViewEvent {
|
||||
val onTopAppBarNavigation: () -> Unit get() = {}
|
||||
val onDevice: (Device) -> Unit get() = { _ -> }
|
||||
val onRefresh: () -> Unit get() = {}
|
||||
val onMoreOptions: (Device) -> Unit get() = { _ -> }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.viewmodel
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.presentation.extension.require
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.device.domain.entity.Device
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
import me.proton.core.drive.drivelink.device.domain.usecase.GetDecryptedDevice
|
||||
import me.proton.core.drive.drivelink.device.presentation.options.DeviceOptionEntry
|
||||
import me.proton.core.drive.drivelink.device.presentation.options.RenameDeviceOption
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ComputerOptionsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getDecryptedDevice: GetDecryptedDevice,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val deviceId = DeviceId(savedStateHandle.require(KEY_DEVICE_ID))
|
||||
val device: Flow<Device?> = getDecryptedDevice(
|
||||
userId = userId,
|
||||
deviceId = deviceId,
|
||||
)
|
||||
.mapSuccessValueOrNull()
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
fun entries(
|
||||
runAction: (suspend () -> Unit) -> Unit,
|
||||
navigateToRenameComputer: (DeviceId, FolderId) -> Unit,
|
||||
): List<DeviceOptionEntry> = listOf(
|
||||
RenameDeviceOption { device ->
|
||||
runAction { navigateToRenameComputer(device.id, device.rootLinkId) }
|
||||
}
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val KEY_DEVICE_ID = "deviceId"
|
||||
}
|
||||
}
|
||||
@@ -45,12 +45,15 @@ import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.device.domain.entity.Device
|
||||
import me.proton.core.drive.device.domain.entity.DeviceId
|
||||
import me.proton.core.drive.device.domain.extension.name
|
||||
import me.proton.core.drive.device.domain.usecase.RefreshDevices
|
||||
import me.proton.core.drive.drivelink.device.domain.usecase.GetDecryptedDevicesSortedByName
|
||||
import me.proton.core.drive.files.presentation.state.ListContentState
|
||||
import me.proton.core.drive.i18n.R
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.drivelink.device.presentation.R as DriveLinkDevicePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
@@ -63,7 +66,11 @@ class ComputersViewModel @Inject constructor(
|
||||
private val refreshDevices: RefreshDevices,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle), HomeTabViewModel {
|
||||
shouldUpgradeStorage: ShouldUpgradeStorage,
|
||||
) : ViewModel(),
|
||||
UserViewModel by UserViewModel(savedStateHandle),
|
||||
HomeTabViewModel,
|
||||
NotificationDotViewModel by NotificationDotViewModel(shouldUpgradeStorage) {
|
||||
|
||||
private val _homeEffect = MutableSharedFlow<HomeEffect>()
|
||||
override val homeEffect: Flow<HomeEffect>
|
||||
@@ -85,6 +92,7 @@ class ComputersViewModel @Inject constructor(
|
||||
val initialViewState = ComputersViewState(
|
||||
title = appContext.getString(R.string.computers_title),
|
||||
navigationIconResId = me.proton.core.presentation.R.drawable.ic_proton_hamburger,
|
||||
notificationDotVisible = false,
|
||||
listContentState = listContentState.value,
|
||||
isRefreshEnabled = listContentState.value != ListContentState.Loading
|
||||
)
|
||||
@@ -92,8 +100,10 @@ class ComputersViewModel @Inject constructor(
|
||||
val viewState: Flow<ComputersViewState> = combine(
|
||||
listContentState,
|
||||
isRefreshing,
|
||||
) { state, refreshing ->
|
||||
notificationDotRequested
|
||||
) { state, refreshing, notificationDotRequested ->
|
||||
initialViewState.copy(
|
||||
notificationDotVisible = notificationDotRequested,
|
||||
listContentState = when (state) {
|
||||
is ListContentState.Content -> state.copy(isRefreshing = refreshing)
|
||||
is ListContentState.Empty -> state.copy(isRefreshing = refreshing)
|
||||
@@ -134,6 +144,7 @@ class ComputersViewModel @Inject constructor(
|
||||
|
||||
fun viewEvent(
|
||||
navigateToSyncedFolders: (FolderId, String?) -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
): ComputersViewEvent = object : ComputersViewEvent {
|
||||
|
||||
override val onTopAppBarNavigation = {
|
||||
@@ -146,6 +157,10 @@ class ComputersViewModel @Inject constructor(
|
||||
navigateToSyncedFolders(device.rootLinkId, name)
|
||||
}
|
||||
|
||||
override val onMoreOptions = { device: Device ->
|
||||
navigateToComputerOptions(device.id)
|
||||
}
|
||||
|
||||
override val onRefresh = {
|
||||
viewModelScope.launch {
|
||||
isRefreshing.value = true
|
||||
|
||||
@@ -84,6 +84,7 @@ import me.proton.core.drive.sorting.domain.entity.Sorting
|
||||
import me.proton.core.drive.sorting.domain.usecase.GetSorting
|
||||
import me.proton.core.drive.upload.domain.usecase.CancelUploadFile
|
||||
import me.proton.core.drive.upload.domain.usecase.GetUploadProgress
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import me.proton.drive.android.settings.domain.entity.LayoutType
|
||||
import me.proton.drive.android.settings.domain.usecase.GetLayoutType
|
||||
@@ -115,7 +116,10 @@ class FilesViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getSorting: GetSorting,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) : SelectionViewModel(savedStateHandle, selectLinks, deselectLinks, selectAll, getSelectedDriveLinks), HomeTabViewModel {
|
||||
shouldUpgradeStorage: ShouldUpgradeStorage,
|
||||
) : SelectionViewModel(savedStateHandle, selectLinks, deselectLinks, selectAll, getSelectedDriveLinks),
|
||||
HomeTabViewModel,
|
||||
NotificationDotViewModel by NotificationDotViewModel(shouldUpgradeStorage) {
|
||||
|
||||
private val shareId = savedStateHandle.get<String>(Screen.Files.SHARE_ID)
|
||||
private val folderId = savedStateHandle.get<String>(Screen.Files.FOLDER_ID)?.let { folderId ->
|
||||
@@ -196,7 +200,8 @@ class FilesViewModel @Inject constructor(
|
||||
listContentAppendingState,
|
||||
layoutType,
|
||||
selected,
|
||||
) { driveLink, sorting, contentState, appendingState, layoutType, selected ->
|
||||
notificationDotRequested
|
||||
) { driveLink, sorting, contentState, appendingState, layoutType, selected, notificationDotRequested ->
|
||||
val listContentState = when (contentState) {
|
||||
is ListContentState.Empty -> contentState.copy(
|
||||
imageResId = emptyStateImageResId,
|
||||
@@ -208,6 +213,7 @@ class FilesViewModel @Inject constructor(
|
||||
} else {
|
||||
topBarActions.value = setOf(selectAllAction, selectedOptionsAction)
|
||||
}
|
||||
val showHamburgerMenuIcon = isRootFolder && selected.isEmpty()
|
||||
initialViewState.copy(
|
||||
title = if (selected.isNotEmpty()) {
|
||||
appContext.quantityString(
|
||||
@@ -220,7 +226,7 @@ class FilesViewModel @Inject constructor(
|
||||
}
|
||||
},
|
||||
isTitleEncrypted = selected.isEmpty() && isRootFolder.not() && driveLink.isNameEncrypted,
|
||||
navigationIconResId = if (isRootFolder && selected.isEmpty()) {
|
||||
navigationIconResId = if (showHamburgerMenuIcon) {
|
||||
CorePresentation.drawable.ic_proton_hamburger
|
||||
} else if (selected.isNotEmpty()) {
|
||||
CorePresentation.drawable.ic_proton_cross
|
||||
@@ -233,6 +239,7 @@ class FilesViewModel @Inject constructor(
|
||||
isGrid = layoutType == LayoutType.GRID,
|
||||
isRefreshEnabled = selected.isEmpty(),
|
||||
drawerGesturesEnabled = isRootFolder && selected.isEmpty(),
|
||||
notificationDotVisible = showHamburgerMenuIcon && notificationDotRequested
|
||||
)
|
||||
}
|
||||
val listEffect: Flow<ListEffect>
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.viewmodel
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import me.proton.android.drive.ui.viewstate.GetMoreFreeStorageViewState
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.domain.arch.DataResult
|
||||
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.asHumanReadableString
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.folder.domain.usecase.HasAnyCachedFolderChildren
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.domain.usecase.GetShares
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
|
||||
@HiltViewModel
|
||||
class GetMoreFreeStorageViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
configurationProvider: ConfigurationProvider,
|
||||
getShares: GetShares,
|
||||
private val hasAnyCachedFolderChildren: HasAnyCachedFolderChildren,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val uploadAFile = GetMoreFreeStorageViewState.Action(
|
||||
iconResId = uploadAFileIconResId(false),
|
||||
titleResId = I18N.string.get_more_free_storage_action_upload_title,
|
||||
getDescription = { AnnotatedString(appContext.getString(I18N.string.get_more_free_storage_action_upload_subtitle)) },
|
||||
isDone = false,
|
||||
)
|
||||
|
||||
private val createAShareLink = GetMoreFreeStorageViewState.Action(
|
||||
iconResId = createAShareLinkIconResId(false),
|
||||
titleResId = I18N.string.get_more_free_storage_action_link_title,
|
||||
getDescription = { createAShareLinkDescription },
|
||||
isDone = false,
|
||||
)
|
||||
|
||||
private val setARecoveryMethod = GetMoreFreeStorageViewState.Action(
|
||||
iconResId = setARecoveryMethodIconResId(false),
|
||||
titleResId = I18N.string.get_more_free_storage_action_recovery_title,
|
||||
getDescription = { setARecoveryMethodDescription(ProtonTheme.colors.textAccent) },
|
||||
isDone = false,
|
||||
onSubtitleClick = ::onSetRecoverMethodSubtitleClick
|
||||
)
|
||||
|
||||
val initialViewState = GetMoreFreeStorageViewState(
|
||||
imageResId = BasePresentation.drawable.img_free_storage,
|
||||
title = appContext.getString(
|
||||
I18N.string.get_more_free_storage_title,
|
||||
configurationProvider.maxFreeSpace.asHumanReadableString(appContext, numberOfDecimals = 0),
|
||||
),
|
||||
descriptionResId = I18N.string.get_more_free_storage_description,
|
||||
actions = listOf(uploadAFile, createAShareLink, setARecoveryMethod),
|
||||
)
|
||||
|
||||
val viewState: Flow<GetMoreFreeStorageViewState> = getShares(userId, Share.Type.STANDARD, flowOf(false))
|
||||
.distinctUntilChanged()
|
||||
.transform { result ->
|
||||
when (result) {
|
||||
is DataResult.Success -> emit(
|
||||
initialViewState.copy(
|
||||
actions = listOf(
|
||||
hasAnyCachedFolderChildren(userId, filesOnly = true).let {
|
||||
uploadAFile.copy(
|
||||
iconResId = uploadAFileIconResId(false),
|
||||
isDone = false,
|
||||
)
|
||||
},
|
||||
result.value.isNotEmpty().let {
|
||||
createAShareLink.copy(
|
||||
iconResId = createAShareLinkIconResId(false),
|
||||
isDone = false,
|
||||
)
|
||||
},
|
||||
setARecoveryMethod,
|
||||
)
|
||||
)
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val isDoneIconResId = CorePresentation.drawable.ic_proton_checkmark
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun uploadAFileIconResId(isDone: Boolean): Int =
|
||||
if (isDone) isDoneIconResId else CorePresentation.drawable.ic_proton_arrow_up_line
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun createAShareLinkIconResId(isDone: Boolean): Int =
|
||||
if (isDone) isDoneIconResId else CorePresentation.drawable.ic_proton_link
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun setARecoveryMethodIconResId(isDone: Boolean): Int =
|
||||
if (isDone) isDoneIconResId else CorePresentation.drawable.ic_proton_key
|
||||
|
||||
private val createAShareLinkDescription: AnnotatedString get() {
|
||||
val getLink = appContext.getString(I18N.string.common_get_link_action)
|
||||
val description = appContext.getString(I18N.string.get_more_free_storage_action_link_subtitle).format(getLink)
|
||||
val start = description.indexOf(getLink)
|
||||
val spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(fontWeight = FontWeight.Bold),
|
||||
start = start,
|
||||
end = start + getLink.length
|
||||
)
|
||||
)
|
||||
return AnnotatedString(text = description, spanStyles = spanStyles)
|
||||
}
|
||||
|
||||
private fun setARecoveryMethodDescription(linkColor: Color): AnnotatedString {
|
||||
val urlString = appContext.getString(I18N.string.get_more_free_storage_action_recovery_subtitle_url_string)
|
||||
val description = appContext
|
||||
.getString(I18N.string.get_more_free_storage_action_recovery_subtitle)
|
||||
.format(urlString)
|
||||
val start = description.indexOf(urlString)
|
||||
val spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(color = linkColor),
|
||||
start = start,
|
||||
end = start + urlString.length
|
||||
)
|
||||
)
|
||||
return AnnotatedString(text = description, spanStyles = spanStyles)
|
||||
}
|
||||
|
||||
private fun onSetRecoverMethodSubtitleClick(offset: Int) {
|
||||
val urlString = appContext.getString(I18N.string.get_more_free_storage_action_recovery_subtitle_url_string)
|
||||
val description = appContext
|
||||
.getString(I18N.string.get_more_free_storage_action_recovery_subtitle)
|
||||
.format(urlString)
|
||||
val start = description.indexOf(urlString)
|
||||
val linkRange = IntRange(
|
||||
start = start,
|
||||
endInclusive = start + urlString.length,
|
||||
)
|
||||
if (offset in linkRange) {
|
||||
try {
|
||||
val url = appContext
|
||||
.getString(I18N.string.get_more_free_storage_action_recovery_subtitle_url)
|
||||
.format(urlString)
|
||||
appContext.startActivity(
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
} catch (ignored: ActivityNotFoundException) {
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = appContext.getString(I18N.string.common_error_no_browser_available),
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -29,12 +29,14 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import me.proton.android.drive.BuildConfig
|
||||
import me.proton.android.drive.ui.navigation.HomeTab
|
||||
import me.proton.android.drive.ui.navigation.Screen
|
||||
import me.proton.android.drive.ui.viewevent.HomeViewEvent
|
||||
import me.proton.android.drive.ui.viewstate.HomeViewState
|
||||
import me.proton.android.drive.usecase.CanGetMoreFreeStorage
|
||||
import me.proton.core.domain.entity.SessionUserId
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.presentation.component.NavigationTab
|
||||
@@ -53,6 +55,7 @@ class HomeViewModel @Inject constructor(
|
||||
userManager: UserManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
configurationProvider: ConfigurationProvider,
|
||||
private val canGetMoreFreeStorage: CanGetMoreFreeStorage,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
|
||||
private val tabs: StateFlow<Map<out HomeTab, NavigationTab>> = MutableStateFlow(
|
||||
@@ -78,7 +81,7 @@ class HomeViewModel @Inject constructor(
|
||||
).associateBy({ tab -> tab.first }, { tab -> tab.second })
|
||||
)
|
||||
|
||||
private val currentDestination = MutableStateFlow(Screen.Files.route)
|
||||
private val currentDestination = MutableStateFlow<String?>(null)
|
||||
fun setCurrentDestination(route: String) {
|
||||
currentDestination.value = route
|
||||
}
|
||||
@@ -86,7 +89,7 @@ class HomeViewModel @Inject constructor(
|
||||
val viewState: Flow<HomeViewState> =
|
||||
combine(
|
||||
userManager.observeUser(SessionUserId(userId.id)),
|
||||
currentDestination,
|
||||
currentDestination.filterNotNull(),
|
||||
tabs.filter { tabs -> tabs.isNotEmpty() },
|
||||
) { user, selectedScreen, tabs ->
|
||||
getViewState(user, selectedScreen, tabs)
|
||||
@@ -100,6 +103,7 @@ class HomeViewModel @Inject constructor(
|
||||
navigateToSettings: () -> Unit,
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToGetMoreFreeStorage: () -> Unit,
|
||||
): HomeViewEvent = object : HomeViewEvent {
|
||||
override val onTab = { tab: NavigationTab -> navigateToTab(tab.screen(userId)) }
|
||||
override val navigationDrawerViewEvent: NavigationDrawerViewEvent =
|
||||
@@ -111,13 +115,14 @@ class HomeViewModel @Inject constructor(
|
||||
override val onSignOut = navigateToSigningOut
|
||||
override val onBugReport = navigateToBugReport
|
||||
override val onSubscription = navigateToSubscription
|
||||
override val onGetFreeStorage = navigateToGetMoreFreeStorage
|
||||
}
|
||||
}
|
||||
|
||||
private val NavigationTab.screen: HomeTab
|
||||
get() = tabs.value.firstNotNullOf { (screen, value) -> screen.takeIf { value == this } }
|
||||
|
||||
private fun getViewState(
|
||||
private suspend fun getViewState(
|
||||
user: User?,
|
||||
startDestination: String,
|
||||
tabs: Map<out HomeTab, NavigationTab>,
|
||||
@@ -130,7 +135,8 @@ class HomeViewModel @Inject constructor(
|
||||
navigationDrawerViewState = NavigationDrawerViewState(
|
||||
I18N.string.app_name,
|
||||
BuildConfig.VERSION_NAME,
|
||||
currentUser = user
|
||||
currentUser = user,
|
||||
showGetFreeStorage = user?.let { canGetMoreFreeStorage(user) } ?: false,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
@@ -50,7 +52,10 @@ import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.share.domain.extension.isDevice
|
||||
import me.proton.core.drive.share.domain.usecase.GetShare
|
||||
import me.proton.core.drive.sorting.domain.entity.Sorting
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import javax.inject.Inject
|
||||
@@ -64,6 +69,7 @@ class MoveToFolderViewModel @Inject constructor(
|
||||
getDriveLink: GetDecryptedDriveLink,
|
||||
getPagedDriveLinks: GetPagedDriveLinksList,
|
||||
getSelectedDriveLinks: GetSelectedDriveLinks,
|
||||
getShare: GetShare,
|
||||
private val moveFile: MoveFile,
|
||||
private val deselectLinks: DeselectLinks,
|
||||
private val decryptDriveLinks: DecryptDriveLinks,
|
||||
@@ -85,6 +91,12 @@ class MoveToFolderViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
private val shareFlow: StateFlow<Share?> =
|
||||
(shareId?.let {
|
||||
getShare(shareId, flowOf(false))
|
||||
.mapSuccessValueOrNull()
|
||||
} ?: flowOf(null))
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
private val driveLinksToMove: StateFlow<List<DriveLink>> = if (selectionId != null) {
|
||||
getSelectedDriveLinks(selectionId)
|
||||
@@ -107,16 +119,21 @@ class MoveToFolderViewModel @Inject constructor(
|
||||
driveLinksToMove,
|
||||
parentLink,
|
||||
listContentState,
|
||||
listContentAppendingState
|
||||
) { driveLinksToMove, parentLink, contentState, appendingState ->
|
||||
listContentAppendingState,
|
||||
shareFlow.filterNotNull(),
|
||||
) { driveLinksToMove, parentLink, contentState, appendingState, share ->
|
||||
val isRoot = parentLink != null && parentLink.parentId == null
|
||||
initialViewState.copy(
|
||||
filesViewState = initialViewState.filesViewState.copy(
|
||||
listContentState = contentState,
|
||||
listContentAppendingState = appendingState
|
||||
),
|
||||
isMoveButtonEnabled = parentLink?.id != parentId,
|
||||
title = if (isRoot) appContext.getString(I18N.string.title_my_files) else parentLink?.name.orEmpty(),
|
||||
isMoveButtonEnabled = (parentLink?.id != parentId) && !(isRoot && share.isDevice),
|
||||
title = if (isRoot && share.isMain) {
|
||||
appContext.getString(I18N.string.title_my_files)
|
||||
} else {
|
||||
parentLink?.name.orEmpty()
|
||||
},
|
||||
isTitleEncrypted = parentLink?.isNameEncrypted ?: false,
|
||||
navigationIconResId = if (parentLink == null || isRoot) {
|
||||
CorePresentation.drawable.ic_proton_cross
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.proton.android.drive.extension.asBoolean
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
|
||||
interface NotificationDotViewModel {
|
||||
val notificationDotRequested: Flow<Boolean>
|
||||
|
||||
companion object {
|
||||
operator fun invoke(shouldUpgradeStorage: ShouldUpgradeStorage) =
|
||||
object : NotificationDotViewModel {
|
||||
override val notificationDotRequested: Flow<Boolean> =
|
||||
shouldUpgradeStorage().map { it.asBoolean }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.photos.domain.entity.PhotoBackupState
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotosConfiguration
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
|
||||
@@ -38,8 +39,8 @@ import me.proton.android.drive.ui.viewevent.PhotosBackupViewEvent
|
||||
import me.proton.android.drive.ui.viewstate.PhotosBackupOption
|
||||
import me.proton.android.drive.ui.viewstate.PhotosBackupViewState
|
||||
import me.proton.core.drive.backup.domain.entity.BackupNetworkType
|
||||
import me.proton.core.drive.base.data.extension.getDefaultMessage
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.exception.DriveException
|
||||
import me.proton.core.drive.base.domain.extension.firstSuccessOrError
|
||||
import me.proton.core.drive.base.domain.extension.toResult
|
||||
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
|
||||
@@ -124,9 +125,14 @@ class PhotosBackupViewModel @Inject constructor(
|
||||
}
|
||||
}.onFailure { error ->
|
||||
error.log(BACKUP)
|
||||
val userError = if (error.cause is DriveException) {
|
||||
error.cause as DriveException
|
||||
} else {
|
||||
error
|
||||
}
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
message = userError.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.viewmodel
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.photos.presentation.viewevent.PhotosUpsellViewEvent
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.log.LogTag.PHOTO
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.telemetry.domain.event.PhotosEvent.UpsellPhotosAccepted
|
||||
import me.proton.core.drive.telemetry.domain.event.PhotosEvent.UpsellPhotosDeclined
|
||||
import me.proton.core.drive.telemetry.domain.manager.DriveTelemetryManager
|
||||
import me.proton.core.drive.user.domain.entity.UserMessage
|
||||
import me.proton.core.drive.user.domain.usecase.CancelUserMessage
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PhotosUpsellViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val cancelUserMessage: CancelUserMessage,
|
||||
private val driveTelemetryManager: DriveTelemetryManager,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
|
||||
fun viewEvent(
|
||||
runAction: RunAction,
|
||||
navigateToSubscription: () -> Unit,
|
||||
): PhotosUpsellViewEvent = object : PhotosUpsellViewEvent {
|
||||
override val onMoreStorage = {
|
||||
runAction {
|
||||
navigateToSubscription()
|
||||
driveTelemetryManager.enqueue(userId, UpsellPhotosAccepted())
|
||||
}
|
||||
}
|
||||
override val onCancel = {
|
||||
runAction {
|
||||
driveTelemetryManager.enqueue(userId, UpsellPhotosDeclined())
|
||||
}
|
||||
}
|
||||
override val onDismiss = {
|
||||
viewModelScope.launch {
|
||||
cancelUserMessage(userId, UserMessage.UPSELL_PHOTOS).onFailure { error ->
|
||||
error.log(PHOTO, "Cannot cancel upsell photos message")
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
@@ -50,6 +51,7 @@ import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.photos.domain.entity.PhotoBackupState
|
||||
import me.proton.android.drive.photos.domain.usecase.EnablePhotosBackup
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
|
||||
import me.proton.android.drive.photos.domain.usecase.ShowUpsell
|
||||
import me.proton.android.drive.photos.presentation.R
|
||||
import me.proton.android.drive.photos.presentation.state.PhotosItem
|
||||
import me.proton.android.drive.photos.presentation.viewevent.PhotosViewEvent
|
||||
@@ -60,12 +62,14 @@ import me.proton.android.drive.photos.presentation.viewstate.PhotosViewState
|
||||
import me.proton.android.drive.ui.common.onClick
|
||||
import me.proton.android.drive.ui.effect.HomeEffect
|
||||
import me.proton.android.drive.ui.effect.HomeTabViewModel
|
||||
import me.proton.android.drive.ui.effect.PhotosEffect
|
||||
import me.proton.android.drive.usecase.OnFilesDriveLinkError
|
||||
import me.proton.core.domain.arch.onSuccess
|
||||
import me.proton.core.drive.backup.domain.entity.BackupPermissions
|
||||
import me.proton.core.drive.backup.domain.entity.BackupStatus
|
||||
import me.proton.core.drive.backup.domain.manager.BackupPermissionsManager
|
||||
import me.proton.core.drive.backup.domain.usecase.GetBackupState
|
||||
import me.proton.core.drive.backup.domain.usecase.GetDisabledBackupState
|
||||
import me.proton.core.drive.backup.domain.usecase.RetryBackup
|
||||
import me.proton.core.drive.backup.domain.usecase.SyncFolders
|
||||
import me.proton.core.drive.base.data.extension.getDefaultMessage
|
||||
@@ -75,6 +79,7 @@ import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
import me.proton.core.drive.base.domain.extension.mapWithPrevious
|
||||
import me.proton.core.drive.base.domain.extension.onFailure
|
||||
import me.proton.core.drive.base.domain.log.LogTag.BACKUP
|
||||
import me.proton.core.drive.base.domain.log.LogTag.PHOTO
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
@@ -102,6 +107,9 @@ import me.proton.core.drive.linkupload.domain.entity.UploadFileLink.Companion.RE
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.photo.domain.usecase.GetPhotoCount
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.user.domain.entity.UserMessage
|
||||
import me.proton.core.drive.user.domain.usecase.CancelUserMessage
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import java.util.Calendar
|
||||
import javax.inject.Inject
|
||||
@@ -125,7 +133,9 @@ class PhotosViewModel @Inject constructor(
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
getBackupState: GetBackupState,
|
||||
getDisabledBackupState: GetDisabledBackupState,
|
||||
getPhotoCount: GetPhotoCount,
|
||||
showUpsell: ShowUpsell,
|
||||
getSelectedDriveLinks: GetSelectedDriveLinks,
|
||||
selectAll: SelectAll,
|
||||
selectLinks: SelectLinks,
|
||||
@@ -134,9 +144,11 @@ class PhotosViewModel @Inject constructor(
|
||||
private val photoDriveLinks: PhotoDriveLinks,
|
||||
private val onFilesDriveLinkError: OnFilesDriveLinkError,
|
||||
private val syncFolders: SyncFolders,
|
||||
) : SelectionViewModel(
|
||||
savedStateHandle, selectLinks, deselectLinks, selectAll, getSelectedDriveLinks
|
||||
), HomeTabViewModel {
|
||||
private val cancelUserMessage: CancelUserMessage,
|
||||
shouldUpgradeStorage: ShouldUpgradeStorage,
|
||||
) : SelectionViewModel(savedStateHandle, selectLinks, deselectLinks, selectAll, getSelectedDriveLinks),
|
||||
HomeTabViewModel,
|
||||
NotificationDotViewModel by NotificationDotViewModel(shouldUpgradeStorage) {
|
||||
|
||||
private var viewEvent: PhotosViewEvent? = null
|
||||
private var fetchingJob: Job? = null
|
||||
@@ -150,6 +162,18 @@ class PhotosViewModel @Inject constructor(
|
||||
private val firstVisibleItemIndex = MutableStateFlow<Int?>(null)
|
||||
private val forceStatusExpand = MutableStateFlow(false)
|
||||
|
||||
private val _photosEffect = MutableSharedFlow<PhotosEffect>()
|
||||
private val photosEffectShowUpsell = showUpsell(userId).transformLatest { show ->
|
||||
if (show) {
|
||||
CoreLogger.d(PHOTO, "photosEffectShowUpsell")
|
||||
emit(PhotosEffect.ShowUpsell)
|
||||
}
|
||||
}
|
||||
val photosEffect: Flow<PhotosEffect> = merge(
|
||||
_photosEffect.asSharedFlow(),
|
||||
photosEffectShowUpsell,
|
||||
)
|
||||
|
||||
val initialViewState = PhotosViewState(
|
||||
title = appContext.getString(I18N.string.photos_title),
|
||||
navigationIconResId = CorePresentation.drawable.ic_proton_hamburger,
|
||||
@@ -263,8 +287,12 @@ class PhotosViewModel @Inject constructor(
|
||||
dayNight = R.drawable.empty_photos_daynight,
|
||||
)
|
||||
|
||||
private val backupState = parentFolderId.filterNotNull().flatMapLatest { folderId ->
|
||||
getBackupState(folderId = folderId)
|
||||
private val backupState = parentFolderId.flatMapLatest { folderId ->
|
||||
if (folderId == null) {
|
||||
getDisabledBackupState()
|
||||
} else {
|
||||
getBackupState(folderId = folderId)
|
||||
}
|
||||
}
|
||||
|
||||
val viewState: Flow<PhotosViewState> = combine(
|
||||
@@ -274,7 +302,8 @@ class PhotosViewModel @Inject constructor(
|
||||
getPhotoCount(userId = userId),
|
||||
firstVisibleItemIndex,
|
||||
forceStatusExpand,
|
||||
) { selected, contentState, backupState, count, firstVisibleItemIndex, forceStatusExpand ->
|
||||
notificationDotRequested
|
||||
) { selected, contentState, backupState, count, firstVisibleItemIndex, forceStatusExpand, notificationDotRequested ->
|
||||
val listContentState = when (contentState) {
|
||||
is ListContentState.Empty -> contentState.copy(
|
||||
imageResId = emptyStateImageResId,
|
||||
@@ -292,6 +321,7 @@ class PhotosViewModel @Inject constructor(
|
||||
val showPhotosStateIndicator = selected.isEmpty() &&
|
||||
((firstVisibleItemIndex?.let { index -> index > 0 } ?: false)
|
||||
|| !isDisableOrRunning)
|
||||
val showHamburgerMenuIcon = selected.isEmpty()
|
||||
initialViewState.copy(
|
||||
title = if (selected.isNotEmpty()) {
|
||||
appContext.quantityString(
|
||||
@@ -301,11 +331,12 @@ class PhotosViewModel @Inject constructor(
|
||||
} else {
|
||||
appContext.getString(I18N.string.photos_title)
|
||||
},
|
||||
navigationIconResId = if (selected.isNotEmpty()) {
|
||||
CorePresentation.drawable.ic_proton_cross
|
||||
} else {
|
||||
navigationIconResId = if (showHamburgerMenuIcon) {
|
||||
CorePresentation.drawable.ic_proton_hamburger
|
||||
} else {
|
||||
CorePresentation.drawable.ic_proton_cross
|
||||
},
|
||||
notificationDotVisible = showHamburgerMenuIcon && notificationDotRequested,
|
||||
listContentState = listContentState,
|
||||
showEmptyList = backupState.isBackupEnabled || backupState.hasDefaultFolder == false ,
|
||||
showPhotosStateIndicator = showPhotosStateIndicator,
|
||||
@@ -324,6 +355,7 @@ class PhotosViewModel @Inject constructor(
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
): PhotosViewEvent = object : PhotosViewEvent {
|
||||
|
||||
@@ -378,11 +410,15 @@ class PhotosViewModel @Inject constructor(
|
||||
override val onIgnoreBackgroundRestrictions: (Context) -> Unit = {context ->
|
||||
context.launchIgnoreBatteryOptimizations()
|
||||
}
|
||||
override val onDismissBackgroundRestrictions: () -> Unit = {
|
||||
dismissBackgroundRestrictions()
|
||||
}
|
||||
override val onResolve: () -> Unit = {
|
||||
parentFolderId.value?.let { folderId ->
|
||||
navigateToPhotosIssues(folderId)
|
||||
}
|
||||
}
|
||||
override val onShowUpsell = navigateToPhotosUpsell
|
||||
}.also { viewEvent ->
|
||||
this.viewEvent = viewEvent
|
||||
}
|
||||
@@ -488,6 +524,14 @@ class PhotosViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissBackgroundRestrictions() {
|
||||
viewModelScope.launch {
|
||||
cancelUserMessage(userId, UserMessage.BACKUP_BATTERY_SETTINGS).onFailure { error ->
|
||||
error.log(BACKUP, "Cannot dismiss battery settings warning")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun BackupStatus.isRunning(): Boolean = when (this) {
|
||||
is BackupStatus.Complete -> totalBackupPhotos > 0
|
||||
else -> true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -592,7 +592,7 @@ class PhotoContentProvider(
|
||||
hasThumbnail = false,
|
||||
activeRevisionId = "",
|
||||
xAttr = null,
|
||||
shareUrlId = null,
|
||||
sharingDetails = null,
|
||||
contentKeyPacket = "",
|
||||
contentKeyPacketSignature = null,
|
||||
photoCaptureTime = captureTime,
|
||||
|
||||
@@ -75,6 +75,7 @@ import me.proton.core.drive.share.domain.usecase.GetMainShare
|
||||
import me.proton.core.drive.share.domain.usecase.GetShare
|
||||
import me.proton.core.drive.sorting.domain.entity.Sorting
|
||||
import me.proton.core.drive.sorting.domain.usecase.GetSorting
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
import me.proton.drive.android.settings.domain.entity.LayoutType
|
||||
import me.proton.drive.android.settings.domain.usecase.GetLayoutType
|
||||
import me.proton.drive.android.settings.domain.usecase.GetThemeStyle
|
||||
@@ -100,7 +101,12 @@ class SharedViewModel @Inject constructor(
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val getShare: GetShare,
|
||||
private val getThemeStyle: GetThemeStyle,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle), HomeTabViewModel {
|
||||
shouldUpgradeStorage: ShouldUpgradeStorage,
|
||||
) : ViewModel(),
|
||||
UserViewModel by UserViewModel(savedStateHandle),
|
||||
HomeTabViewModel,
|
||||
NotificationDotViewModel by NotificationDotViewModel(shouldUpgradeStorage) {
|
||||
|
||||
private val _effects = MutableSharedFlow<HomeEffect>()
|
||||
private val refreshTrigger = MutableSharedFlow<Unit>(replay = 1).apply { tryEmit(Unit) }
|
||||
private val volumeId = refreshTrigger.transformLatest {
|
||||
@@ -149,7 +155,8 @@ class SharedViewModel @Inject constructor(
|
||||
driveLinks,
|
||||
layoutType,
|
||||
getThemeStyle(userId),
|
||||
) { sorting, driveLinks, layoutType, _ ->
|
||||
notificationDotRequested,
|
||||
) { sorting, driveLinks, layoutType, _, notificationDotRequested ->
|
||||
val listContentState = when (val contentState = driveLinks.toListContentState()) {
|
||||
is ListContentState.Empty -> contentState.copy(
|
||||
imageResId = emptyStateImageResId,
|
||||
@@ -161,6 +168,7 @@ class SharedViewModel @Inject constructor(
|
||||
sorting = sorting,
|
||||
listContentState = listContentState,
|
||||
isGrid = layoutType == LayoutType.GRID,
|
||||
notificationDotVisible = notificationDotRequested
|
||||
)
|
||||
)
|
||||
}.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
@@ -24,6 +24,7 @@ import me.proton.core.drive.files.presentation.state.ListContentState
|
||||
data class ComputersViewState(
|
||||
val title: String,
|
||||
@DrawableRes val navigationIconResId: Int,
|
||||
val notificationDotVisible: Boolean,
|
||||
val listContentState: ListContentState,
|
||||
val isRefreshEnabled: Boolean,
|
||||
)
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.viewstate
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
||||
@Immutable
|
||||
data class GetMoreFreeStorageViewState(
|
||||
@DrawableRes val imageResId: Int,
|
||||
val title: String,
|
||||
@StringRes val descriptionResId: Int,
|
||||
val actions: List<Action>,
|
||||
) {
|
||||
data class Action(
|
||||
@DrawableRes val iconResId: Int,
|
||||
@StringRes val titleResId: Int,
|
||||
val getDescription: @Composable () -> AnnotatedString,
|
||||
val isDone: Boolean,
|
||||
val onSubtitleClick: (Int) -> Unit = {},
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.usecase
|
||||
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.user.domain.extension.isFree
|
||||
import me.proton.core.user.domain.entity.User
|
||||
import javax.inject.Inject
|
||||
|
||||
class CanGetMoreFreeStorage @Inject constructor(
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) {
|
||||
|
||||
operator fun invoke(user: User): Boolean = with (user) {
|
||||
isFree && isNotUsingMaxFreeSpace
|
||||
}
|
||||
|
||||
private val User.isNotUsingMaxFreeSpace get() = maxSpace.bytes < configurationProvider.maxFreeSpace
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -61,7 +61,12 @@ class ProcessIntent @Inject constructor(
|
||||
private val validateUploadLimit: ValidateUploadLimit,
|
||||
private val validateExternalUri: ValidateExternalUri,
|
||||
) {
|
||||
operator fun invoke(intent: Intent, deepLinkIntent: MutableSharedFlow<Intent>, accountViewModel: AccountViewModel) =
|
||||
operator fun invoke(
|
||||
intent: Intent,
|
||||
deepLinkIntent: MutableSharedFlow<Intent>,
|
||||
accountViewModel: AccountViewModel,
|
||||
isNewIntent: Boolean = false,
|
||||
) =
|
||||
CoroutineScope(Job() + Dispatchers.IO).launch {
|
||||
with (intent) {
|
||||
getStringExtra(EXTRA_NOTIFICATION_ID)?.let { extraNotificationId ->
|
||||
@@ -69,6 +74,9 @@ class ProcessIntent @Inject constructor(
|
||||
}
|
||||
onActionSend { processActionSend(intent, deepLinkIntent, accountViewModel) }
|
||||
onActionSendMultiple { processActionSendMultiple(intent, deepLinkIntent, accountViewModel) }
|
||||
onActionViewWithData(isNewIntent) {
|
||||
processActionViewWithData(intent, deepLinkIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +128,13 @@ class ProcessIntent @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processActionViewWithData(
|
||||
intent: Intent,
|
||||
deepLinkIntent: MutableSharedFlow<Intent>,
|
||||
) {
|
||||
deepLinkIntent.emit(intent)
|
||||
}
|
||||
|
||||
private suspend fun actionSend(
|
||||
deepLinkIntent: MutableSharedFlow<Intent>,
|
||||
accountViewModel: AccountViewModel,
|
||||
@@ -176,6 +191,12 @@ inline fun <T> Intent.onActionSendMultiple(block: (intent: Intent) -> T): T? = t
|
||||
block(this)
|
||||
}
|
||||
|
||||
inline fun <T> Intent.onActionViewWithData(accept: Boolean, block: (intent: Intent) -> T): T? = takeIf {
|
||||
accept && action == Intent.ACTION_VIEW && data != null
|
||||
}?.let {
|
||||
block(this)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "DEPRECATION")
|
||||
inline fun <reified T : Parcelable> getParcelableExtra(
|
||||
intent: Intent,
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (c) 2024 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>
|
||||
<bool name="core_feature_key_transparency_enabled">false</bool>
|
||||
<bool name="core_feature_account_recovery_enabled">true</bool>
|
||||
<bool name="core_feature_event_manager_worker_repeat_internal_background_by_bucket">true</bool>
|
||||
<bool name="core_feature_notifications_enabled">true</bool>
|
||||
<!-- Show system Notification permission request dialog after login. -->
|
||||
<bool name="core_feature_notifications_permission_request_enabled">false</bool>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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/>.
|
||||
-->
|
||||
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:shortcutId="primary-user-files"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_proton_folder"
|
||||
android:shortcutShortLabel="@string/title_files"
|
||||
android:shortcutLongLabel="@string/title_files">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="drive://proton.me/launcher?redirection=files"/>
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="primary-user-link"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_proton_link"
|
||||
android:shortcutShortLabel="@string/title_shared"
|
||||
android:shortcutLongLabel="@string/title_shared">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="drive://proton.me/launcher?redirection=shared"/>
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:shortcutId="primary-user-computers"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_proton_tv"
|
||||
android:shortcutShortLabel="@string/computers_title"
|
||||
android:shortcutLongLabel="@string/computers_title">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="drive://proton.me/launcher?redirection=computers"/>
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2024 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/>.
|
||||
-->
|
||||
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_proton_folder">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="drive://proton.me/launcher?redirection=files"/>
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_proton_link">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="drive://proton.me/launcher?redirection=shared"/>
|
||||
</shortcut>
|
||||
<shortcut
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_proton_tv">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="drive://proton.me/launcher?redirection=computers"/>
|
||||
</shortcut>
|
||||
</shortcuts>
|
||||
+3
-3
@@ -33,7 +33,7 @@ import me.proton.core.drive.base.domain.entity.Percentage
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.db.test.DriveDatabaseRule
|
||||
import me.proton.core.drive.db.test.myDrive
|
||||
import me.proton.core.drive.db.test.myFiles
|
||||
import me.proton.core.drive.db.test.volumeId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.linkupload.data.db.entity.LinkUploadEntity
|
||||
@@ -73,7 +73,7 @@ class MainTelemetryEventHandlerTest {
|
||||
|
||||
@Before
|
||||
fun setUp() = runTest {
|
||||
folderId = database.myDrive {}
|
||||
folderId = database.myFiles {}
|
||||
val getShare: GetShare = mockk()
|
||||
every { getShare(any(), any()) } answers {
|
||||
val shareId: ShareId = arg(0)
|
||||
@@ -152,7 +152,7 @@ class MainTelemetryEventHandlerTest {
|
||||
val id = database.db.linkUploadDao.insert(
|
||||
LinkUploadEntity(
|
||||
userId = me.proton.core.drive.db.test.userId,
|
||||
volumeId = volumeId,
|
||||
volumeId = volumeId.id,
|
||||
shareId = folderId.shareId.id,
|
||||
parentId = folderId.id,
|
||||
name = "",
|
||||
|
||||
+2
-2
@@ -155,7 +155,7 @@ class PhotoTelemetryEventHandlerTest {
|
||||
val id = database.db.linkUploadDao.insert(
|
||||
LinkUploadEntity(
|
||||
userId = me.proton.core.drive.db.test.userId,
|
||||
volumeId = volumeId,
|
||||
volumeId = volumeId.id,
|
||||
shareId = folderId.shareId.id,
|
||||
parentId = folderId.id,
|
||||
name = "",
|
||||
@@ -186,7 +186,7 @@ class PhotoTelemetryEventHandlerTest {
|
||||
val id = database.db.linkUploadDao.insert(
|
||||
LinkUploadEntity(
|
||||
userId = me.proton.core.drive.db.test.userId,
|
||||
volumeId = volumeId,
|
||||
volumeId = volumeId.id,
|
||||
shareId = folderId.shareId.id,
|
||||
parentId = folderId.id,
|
||||
name = "",
|
||||
|
||||
@@ -60,7 +60,6 @@ import me.proton.core.drive.stats.domain.usecase.GetUploadStats
|
||||
import me.proton.core.drive.stats.domain.usecase.IsInitialBackup
|
||||
import me.proton.core.drive.stats.domain.usecase.SetOrIgnoreInitialBackup
|
||||
import me.proton.core.drive.stats.domain.usecase.UpdateUploadStats
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertThrows
|
||||
@@ -203,7 +202,7 @@ class StatsEventHandlerTest {
|
||||
fileCreationDateTime: TimestampS,
|
||||
) = UploadFileLink(
|
||||
userId = userId,
|
||||
volumeId = VolumeId(volumeId),
|
||||
volumeId = volumeId,
|
||||
shareId = folderId.shareId,
|
||||
parentLinkId = folderId,
|
||||
uriString = "uri$index",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.viewmodel
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filterNot
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.core.drive.base.domain.extension.asSuccess
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.device.domain.entity.Device
|
||||
import me.proton.core.drive.device.domain.usecase.RefreshDevices
|
||||
import me.proton.core.drive.drivelink.device.domain.usecase.GetDecryptedDevicesSortedByName
|
||||
import me.proton.core.drive.files.presentation.state.ListContentState
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
import me.proton.core.test.kotlin.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import me.proton.core.drive.drivelink.device.presentation.R as DriveLinkDevicePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class ComputersViewModelTest {
|
||||
private lateinit var computersViewModel: ComputersViewModel
|
||||
private val getDevices: GetDecryptedDevicesSortedByName = mockk<GetDecryptedDevicesSortedByName>()
|
||||
private val savedStateHandle: SavedStateHandle = mockk<SavedStateHandle>()
|
||||
private val refreshDevices: RefreshDevices = mockk<RefreshDevices>()
|
||||
private val broadcastMessages: BroadcastMessages = mockk<BroadcastMessages>()
|
||||
private val configurationProvider: ConfigurationProvider = mockk<ConfigurationProvider>()
|
||||
private val shouldUpgradeStorage: ShouldUpgradeStorage = mockk<ShouldUpgradeStorage>()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
coEvery { savedStateHandle.get<String>(any()) } returns "saved_state_handle_key_value"
|
||||
coEvery { getDevices.invoke(any()) } returns flowOf(emptyList<Device>().asSuccess)
|
||||
coEvery { shouldUpgradeStorage() } returns flowOf(ShouldUpgradeStorage.Result.NoUpgrade)
|
||||
|
||||
computersViewModel = ComputersViewModel(
|
||||
appContext = ApplicationProvider.getApplicationContext(),
|
||||
getDevices = getDevices,
|
||||
savedStateHandle = savedStateHandle,
|
||||
refreshDevices = refreshDevices,
|
||||
broadcastMessages = broadcastMessages,
|
||||
configurationProvider = configurationProvider,
|
||||
shouldUpgradeStorage = shouldUpgradeStorage,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty computers tab resources check`() = runTest {
|
||||
// Given
|
||||
launch {
|
||||
computersViewModel.devices.collect()
|
||||
}
|
||||
|
||||
// When
|
||||
val viewState = computersViewModel
|
||||
.viewState
|
||||
.filterNot { viewState -> viewState.listContentState is ListContentState.Loading }
|
||||
.first()
|
||||
|
||||
// Then
|
||||
val emptyState = viewState.listContentState as ListContentState.Empty
|
||||
assertEquals(
|
||||
DriveLinkDevicePresentation.drawable.empty_devices_daynight,
|
||||
emptyState.imageResId,
|
||||
) { "Wrong empty image resource" }
|
||||
assertEquals(
|
||||
I18N.string.computers_empty_title,
|
||||
emptyState.titleId,
|
||||
) { "Wrong empty title resource" }
|
||||
assertEquals(
|
||||
I18N.string.computers_empty_description,
|
||||
emptyState.descriptionResId,
|
||||
) { "Wrong empty description resource" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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 dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import me.proton.android.drive.repository.TestFeatureFlagRepositoryImpl
|
||||
import me.proton.core.drive.feature.flag.data.di.FeatureFlagBindModule
|
||||
import me.proton.core.drive.feature.flag.data.repository.FeatureFlagRepositoryImpl
|
||||
import me.proton.core.drive.feature.flag.domain.repository.FeatureFlagRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [FeatureFlagBindModule::class]
|
||||
)
|
||||
object TestFeatureFlagBindModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFeatureFlagRepository(
|
||||
impl : FeatureFlagRepositoryImpl
|
||||
): FeatureFlagRepository = TestFeatureFlagRepositoryImpl(impl)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.extension
|
||||
|
||||
import me.proton.android.drive.settings.DebugSettings
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
|
||||
val ConfigurationProvider.debug : DebugSettings
|
||||
get() = this as DebugSettings
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.repository
|
||||
|
||||
import me.proton.core.drive.base.domain.log.LogTag.FEATURE_FLAG
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId
|
||||
import me.proton.core.drive.feature.flag.domain.repository.FeatureFlagRepository
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
class TestFeatureFlagRepositoryImpl @Inject constructor(
|
||||
private val repository: FeatureFlagRepository,
|
||||
) : FeatureFlagRepository by repository {
|
||||
|
||||
override suspend fun getFeatureFlag(featureFlagId: FeatureFlagId): FeatureFlag? =
|
||||
(flags[featureFlagId.id]?.let { state ->
|
||||
CoreLogger.d(FEATURE_FLAG, "Overriding feature flag $featureFlagId to $state")
|
||||
FeatureFlag(featureFlagId, state)
|
||||
} ?: repository.getFeatureFlag(featureFlagId))
|
||||
|
||||
companion object {
|
||||
val flags = mutableMapOf<String, FeatureFlag.State>()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.annotation
|
||||
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class FeatureFlag(
|
||||
val id: String,
|
||||
val state: FeatureFlag.State,
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -21,9 +21,9 @@ package me.proton.android.drive.ui.annotation
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Quota(
|
||||
val value: Int = 500,
|
||||
val value: Int = 2_048,
|
||||
val unit: QuotaUnit = QuotaUnit.MB,
|
||||
val percentageFull: Int = 0
|
||||
)
|
||||
|
||||
enum class QuotaUnit { KB, MB, GB, TB, PB }
|
||||
enum class QuotaUnit { KB, MB, GB, TB, PB }
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.extension
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.test.espresso.intent.OngoingStubbing
|
||||
import java.io.File
|
||||
|
||||
|
||||
|
||||
fun OngoingStubbing.respondWithFile(file: File) {
|
||||
respondWithFunction {
|
||||
Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().setData(Uri.fromFile(file)))
|
||||
}
|
||||
}
|
||||
|
||||
fun OngoingStubbing.respondWithFiles(files: List<File>) {
|
||||
respondWithFunction {
|
||||
Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().apply {
|
||||
val items = files.map { file ->
|
||||
ClipData.Item(Uri.fromFile(file))
|
||||
}
|
||||
clipData = ClipData(
|
||||
ClipDescription(
|
||||
"", files.map { "text/plain" }.toTypedArray()
|
||||
),
|
||||
items.first()
|
||||
).also { clipData ->
|
||||
(items - items.first()).forEach(clipData::addItem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -21,11 +21,7 @@ package me.proton.android.drive.ui.extension
|
||||
import me.proton.core.test.quark.data.User
|
||||
import me.proton.core.test.quark.v2.QuarkCommand
|
||||
import me.proton.core.test.quark.v2.toEncodedArgs
|
||||
import me.proton.core.util.kotlin.EMPTY_STRING
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
fun QuarkCommand.populate(
|
||||
user: User,
|
||||
@@ -34,12 +30,12 @@ fun QuarkCommand.populate(
|
||||
): Response =
|
||||
route("quark/drive:populate")
|
||||
.args(
|
||||
listOf(
|
||||
listOfNotNull(
|
||||
"-u" to user.name,
|
||||
"-p" to user.password,
|
||||
"-S" to user.dataSetScenario,
|
||||
"--d" to if (isDevice) true.toString() else EMPTY_STRING,
|
||||
"--photo" to if (isPhotos) true.toString() else EMPTY_STRING
|
||||
isDevice.optionalArg("-d"),
|
||||
isPhotos.optionalArg("--photo"),
|
||||
).toEncodedArgs()
|
||||
)
|
||||
.build()
|
||||
@@ -47,6 +43,10 @@ fun QuarkCommand.populate(
|
||||
client.executeQuarkRequest(it)
|
||||
}
|
||||
|
||||
private fun Boolean.optionalArg(
|
||||
name: String,
|
||||
): Pair<String, String>? = takeIf { it }?.let { name to it.toString() }
|
||||
|
||||
fun QuarkCommand.quotaSetUsedSpace(
|
||||
user: User,
|
||||
usedSpace: String,
|
||||
@@ -64,18 +64,16 @@ fun QuarkCommand.quotaSetUsedSpace(
|
||||
}
|
||||
|
||||
fun QuarkCommand.volumeCreate(
|
||||
user: User,
|
||||
maxSize: String
|
||||
user: User
|
||||
): Response =
|
||||
route("quark/drive:volume:create")
|
||||
.args(
|
||||
listOf(
|
||||
"--username" to user.name,
|
||||
"--pass" to user.password,
|
||||
"--max-size" to maxSize
|
||||
).toEncodedArgs()
|
||||
)
|
||||
.build()
|
||||
.let {
|
||||
client.executeQuarkRequest(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.robot
|
||||
|
||||
import me.proton.android.drive.ui.dialog.ComputerOptionsTestTag
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object ComputerOptionsRobot : Robot {
|
||||
private val computerOptionsScreen get() = node.withTag(ComputerOptionsTestTag.computerOptions)
|
||||
private val renameButton get() = node.withText(I18N.string.common_rename_action)
|
||||
|
||||
fun clickRename() = RenameRobot.apply {
|
||||
renameButton.scrollTo().click()
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
computerOptionsScreen.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.robot
|
||||
|
||||
import me.proton.android.drive.ui.screen.SyncedFoldersTestTag
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object ComputerSyncedFoldersRobot : LinksRobot {
|
||||
private val syncedFoldersScreen get() = node.withTag(SyncedFoldersTestTag.screen)
|
||||
|
||||
fun assertEmptySyncedFolders() {
|
||||
node.withText(I18N.string.computers_synced_folders_empty_title).await { assertIsDisplayed() }
|
||||
node.withText(I18N.string.computers_synced_folders_empty_description).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
syncedFoldersScreen.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.robot
|
||||
|
||||
import me.proton.android.drive.ui.screen.ComputersTestTag
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.test.fusion.FusionConfig.Compose.targetContext
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object ComputersTabRobot : NavigationBarRobot, HomeRobot, LinksRobot, GrowlerRobot, Robot {
|
||||
private val content get() = node.withTag(ComputersTestTag.content)
|
||||
|
||||
private fun moreButton(name: String) = node
|
||||
.withContentDescription(
|
||||
targetContext.getString(I18N.string.computers_content_description_list_more_options, name)
|
||||
)
|
||||
|
||||
fun scrollToComputer(name: String): ComputersTabRobot = apply {
|
||||
content.scrollTo(node.withText(name))
|
||||
}
|
||||
|
||||
fun clickMoreOnComputer(name: String) =
|
||||
moreButton(name).clickTo(ComputerOptionsRobot)
|
||||
|
||||
fun clickOnComputer(name: String): ComputerSyncedFoldersRobot =
|
||||
node
|
||||
.withText(name)
|
||||
.clickTo(ComputerSyncedFoldersRobot)
|
||||
|
||||
fun assertEmptyComputers() {
|
||||
node.withText(I18N.string.computers_empty_title).await { assertIsDisplayed() }
|
||||
node.withText(I18N.string.computers_empty_description).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun itemIsDisplayed(name: String) = node.withText(name).await { assertIsDisplayed() }
|
||||
|
||||
override fun robotDisplayed() {
|
||||
homeScreenDisplayed()
|
||||
computersTab.assertIsSelected()
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,12 @@ interface HomeRobot : Robot {
|
||||
val homeScreen get() = node.withTag(HomeScreenTestTag.screen)
|
||||
val filesTab get() = tabWithText(I18N.string.title_files)
|
||||
val photosTab get() = tabWithText(I18N.string.photos_title)
|
||||
val computersTab get() = tabWithText(I18N.string.computers_title)
|
||||
val sharedTab get() = tabWithText(I18N.string.title_shared)
|
||||
|
||||
fun clickFilesTab() = filesTab.clickTo(FilesTabRobot)
|
||||
|
||||
fun clickPhotosTab() = photosTab.clickTo(PhotosTabRobot)
|
||||
fun clickComputersTab() = computersTab.clickTo(ComputersTabRobot)
|
||||
fun clickSharedTab() = sharedTab.clickTo(SharedTabRobot)
|
||||
|
||||
fun openSidebarBySwipe() = SidebarRobot.apply {
|
||||
|
||||
@@ -20,24 +20,42 @@ package me.proton.android.drive.ui.robot
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import me.proton.android.drive.ui.MainActivity
|
||||
import me.proton.test.fusion.FusionConfig.targetContext
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
object LauncherRobot {
|
||||
/**
|
||||
* Deeplink to any route in MainActivity with an intent VIEW
|
||||
* Test class should inherit from EmptyBaseTest to use it.
|
||||
* @param redirection the redirection to go to
|
||||
*/
|
||||
fun <T : Robot> deeplinkTo(route: String, robot: T): T {
|
||||
ActivityScenario.launch<MainActivity>(Intent(targetContext, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = Uri.parse("drive://proton.me/$route")
|
||||
})
|
||||
return robot
|
||||
}
|
||||
/**
|
||||
* Launch the MainActivity with an intent VIEW and the redirection.
|
||||
* Test class should inherit from EmptyBaseTest to use it.
|
||||
* @param redirection the redirection to go to
|
||||
*/
|
||||
fun <T : Robot> launch(redirection: String, robot: T): T {
|
||||
return deeplinkTo("launcher?redirection=$redirection", robot)
|
||||
}
|
||||
/**
|
||||
* Launch the MainActivity with an intent SEND and the file.
|
||||
* Test class should inherit from EmptyBaseTest to use it.
|
||||
* @param file the file to upload
|
||||
*/
|
||||
fun uploadTo(file: File): UploadToRobot {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
ActivityScenario.launch<MainActivity>(Intent(context, MainActivity::class.java).apply {
|
||||
ActivityScenario.launch<MainActivity>(Intent(targetContext, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_SEND
|
||||
putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file))
|
||||
})
|
||||
@@ -49,8 +67,7 @@ object LauncherRobot {
|
||||
* @param files the files to upload
|
||||
*/
|
||||
fun uploadTo(files: List<File>): UploadToRobot {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
ActivityScenario.launch<MainActivity>(Intent(context, MainActivity::class.java).apply {
|
||||
ActivityScenario.launch<MainActivity>(Intent(targetContext, MainActivity::class.java).apply {
|
||||
val uris = files.map { file -> Uri.fromFile(file) }
|
||||
action = Intent.ACTION_SEND_MULTIPLE
|
||||
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(uris))
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.proton.android.drive.ui.extension.assertDownloadState
|
||||
import me.proton.android.drive.ui.extension.assertHasItemType
|
||||
import me.proton.android.drive.ui.extension.assertHasLayoutType
|
||||
@@ -32,18 +34,20 @@ import me.proton.core.drive.files.presentation.component.files.FilesListHeaderTe
|
||||
import me.proton.core.drive.files.presentation.extension.ItemType
|
||||
import me.proton.core.drive.files.presentation.extension.LayoutType
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
import me.proton.core.drive.i18n.R
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
interface LinksRobot : Robot {
|
||||
|
||||
private val content get() = node.withTag(FilesTestTag.content)
|
||||
val filesContent get() = node.withTag(FilesTestTag.content)
|
||||
|
||||
private val layoutSwitcher get() = node.withTag(FilesListHeaderTestTag.layoutSwitcher)
|
||||
|
||||
private val selectedOptionsButton get() = node
|
||||
.withContentDescription(R.string.content_description_selected_options)
|
||||
.withContentDescription(I18N.string.content_description_selected_options)
|
||||
|
||||
fun linkWithName(name: String) = node.withLinkName(name).isClickable()
|
||||
|
||||
@@ -53,7 +57,7 @@ interface LinksRobot : Robot {
|
||||
fun clickLayoutSwitcher() = layoutSwitcher.clickTo(FilesTabRobot)
|
||||
|
||||
fun scrollToItemWithName(itemName: String): LinksRobot = apply {
|
||||
content.scrollTo(linkWithName(itemName))
|
||||
filesContent.scrollTo(linkWithName(itemName))
|
||||
}
|
||||
|
||||
fun clickOptions() =
|
||||
@@ -90,6 +94,11 @@ interface LinksRobot : Robot {
|
||||
fun clickOnFile(name: String, layoutType: LayoutType = LayoutType.List) =
|
||||
clickOnItem(name, layoutType, ItemType.File, PreviewRobot)
|
||||
|
||||
fun <T : Robot> clickOnUndo(after: Duration = 0.seconds, goesTo: T): T = runBlocking {
|
||||
delay(after)
|
||||
node.withText(I18N.string.common_undo_action).clickTo(goesTo)
|
||||
}
|
||||
|
||||
fun longClickOnItem(name: String) =
|
||||
linkWithName(name).longClickTo(FilesTabRobot)
|
||||
|
||||
|
||||
@@ -21,8 +21,6 @@ package me.proton.android.drive.ui.robot
|
||||
import me.proton.android.drive.ui.screen.MoveToFolderScreenTestTag
|
||||
import me.proton.core.drive.base.presentation.component.TopAppBarComponentTestTag
|
||||
import me.proton.core.drive.files.presentation.component.FilesTestTag.listDetailsTitle
|
||||
import me.proton.core.drive.files.presentation.extension.ItemType
|
||||
import me.proton.core.drive.files.presentation.extension.LayoutType
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.test.fusion.ui.compose.builders.OnNode
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
@@ -33,10 +31,12 @@ object MoveToFolderRobot : NavigationBarRobot, Robot, LinksRobot, GrowlerRobot {
|
||||
private val rootDirectoryTitle = node.withText(I18N.string.title_my_files)
|
||||
private val addFolderButton get() = node.withTag(MoveToFolderScreenTestTag.plusFolderButton)
|
||||
private val cancelButton get() = node.withText(I18N.string.move_file_dismiss_action)
|
||||
private val moveButton get() = node.withText(I18N.string.move_file_confirm_action).isEnabled()
|
||||
private val moveButton get() = node.withTag(MoveToFolderScreenTestTag.moveButton)
|
||||
|
||||
private fun itemWithName(name: String) = node.withTag(listDetailsTitle).withText(name)
|
||||
|
||||
fun assertMoveButtonIsDisabled() = moveButton.assertDisabled()
|
||||
|
||||
/**
|
||||
* Wait the folder to be loaded before clicking back,
|
||||
* to ensure that parent id is known
|
||||
@@ -74,6 +74,7 @@ object MoveToFolderRobot : NavigationBarRobot, Robot, LinksRobot, GrowlerRobot {
|
||||
override fun robotDisplayed() {
|
||||
moveToFolderScreen.assertIsDisplayed()
|
||||
addFolderButton.assertIsDisplayed()
|
||||
filesContent.assertIsDisplayed()
|
||||
cancelButton.assertIsDisplayed()
|
||||
moveButton.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -23,10 +23,12 @@ import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object OfflineRobot : Robot, LinksRobot, NavigationBarRobot {
|
||||
private val offlineScreen get() = node.withText(I18N.string.title_offline_available)
|
||||
private val emptyOfflineScreen get() = node.withText(I18N.string.title_empty_offline_available)
|
||||
private val emptyOfflineTitle get() = node.withText(I18N.string.title_empty_offline_available)
|
||||
private val emptyOfflineDescription get() = node.withText(I18N.string.description_empty_offline_available)
|
||||
|
||||
fun assertIsEmpty() = emptyOfflineScreen.await {
|
||||
assertIsDisplayed()
|
||||
fun assertIsEmpty() {
|
||||
emptyOfflineTitle.await { assertIsDisplayed() }
|
||||
emptyOfflineDescription.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -21,10 +21,10 @@ package me.proton.android.drive.ui.robot
|
||||
import android.os.Build
|
||||
import me.proton.android.drive.ui.extension.withTextResource
|
||||
import me.proton.test.fusion.Fusion.byObject
|
||||
import me.proton.test.fusion.Fusion.device
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
|
||||
object PhotosNoPermissionsRobot: Robot {
|
||||
private val photoPermissionsTitle =
|
||||
node.withTextResource(I18N.string.photos_permission_rational_title, I18N.string.app_name)
|
||||
@@ -35,11 +35,13 @@ object PhotosNoPermissionsRobot: Robot {
|
||||
private val allowAccessImage = node.withText(I18N.string.photos_permission_rational_img_text)
|
||||
|
||||
fun denyPermissions() = apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
device.pressBack()
|
||||
} else {
|
||||
byObject.withText("DENY").waitForClickable().click()
|
||||
}
|
||||
byObject.withTextMatches(
|
||||
when (Build.VERSION.SDK_INT) {
|
||||
in 24..28 -> "DENY"
|
||||
in 29..30 -> "Deny"
|
||||
else -> "Don.+t allow"
|
||||
}.toPattern()
|
||||
).waitForClickable().click()
|
||||
}
|
||||
|
||||
fun <T: Robot> clickNotNow(goesTo: T) = notNowButton.clickTo(goesTo)
|
||||
@@ -51,4 +53,4 @@ object PhotosNoPermissionsRobot: Robot {
|
||||
settingsButton.await { assertIsDisplayed() }
|
||||
allowAccessImage.await { assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ object PhotosTabRobot : HomeRobot, LinksRobot, NavigationBarRobot {
|
||||
node.withTextResource(I18N.string.storage_quotas_not_full_title, "${percentage}%")
|
||||
private val itCanTakeAWhileLabel get() = node.withText(I18N.string.photos_empty_loading_label_progress)
|
||||
private val closeQuotaBannerButton get() = node.withContentDescription(I18N.string.common_close_action)
|
||||
private val photosBackupDisabled get() = node.withText(I18N.string.error_creating_photo_share_not_allowed)
|
||||
private fun itemsLeft(items: Int) =
|
||||
node.withPluralTextResource(I18N.plurals.notification_content_text_backup_in_progress, items)
|
||||
|
||||
@@ -145,6 +146,10 @@ object PhotosTabRobot : HomeRobot, LinksRobot, NavigationBarRobot {
|
||||
itemsLeft(items).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertPhotosBackupDisabled() {
|
||||
photosBackupDisabled.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
homeScreenDisplayed()
|
||||
photosTab.assertIsSelected()
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.robot
|
||||
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object PhotosUpsellRobot : HomeRobot, LinksRobot, NavigationBarRobot {
|
||||
|
||||
private val title get() = node.withText(I18N.string.photos_upsell_title)
|
||||
private val description get() = node.withText(I18N.string.photos_upsell_description)
|
||||
private val getStorage get() = node.withText(I18N.string.photos_upsell_get_storage_action)
|
||||
fun clickGetStorage() = getStorage.clickTo(PhotosTabRobot)
|
||||
|
||||
override fun robotDisplayed() {
|
||||
title.await { assertIsDisplayed() }
|
||||
description.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun robotDoesNotExist() {
|
||||
title.assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -18,10 +18,13 @@
|
||||
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import androidx.compose.ui.layout.boundsInParent
|
||||
import androidx.compose.ui.semantics.SemanticsActions.PageDown
|
||||
import androidx.compose.ui.semantics.SemanticsActions.PageLeft
|
||||
import androidx.compose.ui.semantics.SemanticsActions.PageRight
|
||||
import androidx.compose.ui.semantics.SemanticsActions.PageUp
|
||||
import androidx.compose.ui.semantics.SemanticsActions.ScrollBy
|
||||
import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import me.proton.core.drive.base.presentation.component.TopAppBarComponentTestTag
|
||||
import me.proton.core.drive.files.preview.presentation.component.ImagePreviewComponentTestTag
|
||||
@@ -65,19 +68,54 @@ object PreviewRobot : NavigationBarRobot {
|
||||
pager.swipePage(direction)
|
||||
}
|
||||
|
||||
fun scrollTo(index: Int) = apply {
|
||||
pager.scrollTo(index)
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
previewScreen.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
fun NodeActions.swipePage(direction: SwipeDirection) = apply {
|
||||
val action = when(direction) {
|
||||
val action = when (direction) {
|
||||
SwipeDirection.Left -> PageRight
|
||||
SwipeDirection.Right -> PageLeft
|
||||
SwipeDirection.Up -> PageDown
|
||||
SwipeDirection.Down -> PageUp
|
||||
}
|
||||
waitFor {
|
||||
interaction.performSemanticsAction(action)
|
||||
val semanticsNode = interaction.fetchSemanticsNode()
|
||||
if (semanticsNode.config.contains(action)) {
|
||||
waitFor {
|
||||
interaction.performSemanticsAction(action)
|
||||
}
|
||||
} else {
|
||||
val viewport = semanticsNode.layoutInfo.coordinates.boundsInParent().size
|
||||
val (x, y) = when (direction) {
|
||||
SwipeDirection.Left -> viewport.width to 0F
|
||||
SwipeDirection.Right -> -viewport.width to 0F
|
||||
SwipeDirection.Up -> 0F to viewport.height
|
||||
SwipeDirection.Down -> 0F to -viewport.height
|
||||
}
|
||||
waitFor {
|
||||
scrollBy(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NodeActions.scrollTo(index: Int) = apply {
|
||||
waitFor {
|
||||
interaction.performSemanticsAction(ScrollToIndex) {action ->
|
||||
action(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun NodeActions.scrollBy(x: Float, y: Float) = apply {
|
||||
waitFor {
|
||||
interaction.performSemanticsAction(ScrollBy) {action ->
|
||||
action(x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,20 +19,34 @@
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import me.proton.android.drive.ui.extension.doesNotExist
|
||||
import me.proton.android.drive.ui.robot.settings.GetMoreFreeStorageRobot
|
||||
import me.proton.android.drive.ui.screen.HomeScreenTestTag
|
||||
import me.proton.core.drive.navigationdrawer.presentation.NavigationDrawerTestTag
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object SidebarRobot : Robot {
|
||||
|
||||
private val sidebar get() = node.withTag(HomeScreenTestTag.sidebar)
|
||||
private val content get() = node.withTag(NavigationDrawerTestTag.content)
|
||||
private val storageIndicator get() = node.withTag(NavigationDrawerTestTag.storageIndicator)
|
||||
private val trashNavigationItem get() = node.withText(I18N.string.navigation_item_trash)
|
||||
private val offlineNavigationItem get() = node.withText(I18N.string.navigation_item_offline)
|
||||
private val getMoreFreeStorageItem get() = node.withText(I18N.string.navigation_item_get_free_storage)
|
||||
|
||||
private fun clickSidebarMenuItem(@StringRes menuItemName: Int) {
|
||||
node.withText(menuItemName).click()
|
||||
}
|
||||
|
||||
fun scrollToItemWithName(itemName: String): SidebarRobot = apply {
|
||||
content.scrollTo(node.withText(itemName))
|
||||
}
|
||||
|
||||
fun scrollToStorageIndicator(): SidebarRobot = apply {
|
||||
content.scrollTo(storageIndicator)
|
||||
}
|
||||
|
||||
fun clickReportBug() {
|
||||
clickSidebarMenuItem(I18N.string.navigation_item_bug_report)
|
||||
}
|
||||
@@ -49,6 +63,12 @@ object SidebarRobot : Robot {
|
||||
clickSidebarMenuItem(I18N.string.navigation_item_settings)
|
||||
}
|
||||
|
||||
fun clickGetMoreFreeStorage() = getMoreFreeStorageItem.clickTo(GetMoreFreeStorageRobot)
|
||||
|
||||
fun assertGetMoreFreeStorageIsNotDisplayed() {
|
||||
getMoreFreeStorageItem.await { doesNotExist() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
sidebar.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.robot
|
||||
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object StorageFullRobot : Robot {
|
||||
private val title get() = node.withText(I18N.string.files_upload_failure_storage_full_title)
|
||||
private val dismissButton get() = node.withText(I18N.string.common_got_it_action)
|
||||
|
||||
fun <T : Robot> clickDismiss(goesTo: T) = dismissButton.clickTo(goesTo)
|
||||
|
||||
override fun robotDisplayed() {
|
||||
title.await { assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
@@ -42,10 +42,6 @@ object UploadToRobot : LinksRobot, GrowlerRobot, Robot {
|
||||
).format(count, folderName)
|
||||
).await { assertIsDisplayed() }
|
||||
|
||||
fun assertStorageFull() = node.withText(
|
||||
targetContext.resources.getString(I18N.string.files_upload_failure_storage_full_title)
|
||||
).await { assertIsDisplayed() }
|
||||
|
||||
override fun robotDisplayed() {
|
||||
uploadToScreen.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.robot.settings
|
||||
|
||||
import me.proton.android.drive.ui.robot.Robot
|
||||
import me.proton.android.drive.ui.screen.GetMoreFreeStorage
|
||||
import me.proton.core.drive.base.domain.entity.Bytes
|
||||
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.test.fusion.FusionConfig.Compose.targetContext
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object GetMoreFreeStorageRobot : Robot {
|
||||
private val content = node.withTag(GetMoreFreeStorage.screen)
|
||||
|
||||
fun assertTitleDisplayed(maxFreeSpace: Bytes) {
|
||||
val title = targetContext.getString(
|
||||
I18N.string.get_more_free_storage_title,
|
||||
maxFreeSpace.asHumanReadableString(targetContext, numberOfDecimals = 0),
|
||||
)
|
||||
node.withText(title).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertSubtitleDisplayed() {
|
||||
node.withText(I18N.string.get_more_free_storage_description).await {
|
||||
assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
fun assertActionsDisplayed() {
|
||||
listOf(
|
||||
I18N.string.get_more_free_storage_action_upload_title,
|
||||
I18N.string.get_more_free_storage_action_link_title,
|
||||
I18N.string.get_more_free_storage_action_recovery_title,
|
||||
).forEach { actionTitleResId ->
|
||||
assertAction(actionTitleResId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertAction(titleResId: Int) {
|
||||
node.withText(titleResId).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
content.await { assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ object PhotosBackupRobot : Robot, NavigationBarRobot {
|
||||
private val confirmButton =
|
||||
node.withText(I18N.string.settings_photos_backup_folders_confirm_stop_sync_confirm_action)
|
||||
|
||||
private val photosBackupDisabled get() = node.withText(I18N.string.error_creating_photo_share_not_allowed)
|
||||
|
||||
fun <T : Robot> clickBackupToggle(goesTo: T) = photosBackupToggle.clickTo(goesTo)
|
||||
|
||||
fun assertFoldersList() = uploadFrom.await { assertIsDisplayed() }
|
||||
@@ -49,6 +51,10 @@ object PhotosBackupRobot : Robot, NavigationBarRobot {
|
||||
fun clickFolder(name: String) = node.withText(name).clickTo(PhotosBackupRobot)
|
||||
fun clickConfirm() = confirmButton.clickTo(PhotosBackupRobot)
|
||||
|
||||
fun assertPhotosBackupDisabled() {
|
||||
photosBackupDisabled.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
photosBackupToggle.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import me.proton.core.drive.backup.domain.manager.BackupConnectivityManager
|
||||
import me.proton.core.drive.backup.domain.manager.BackupConnectivityManager.Connectivity.NONE
|
||||
import me.proton.core.drive.backup.domain.manager.BackupConnectivityManager.Connectivity.UNMETERED
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import me.proton.test.fusion.FusionConfig
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.rules.ExternalResource
|
||||
@@ -56,7 +57,7 @@ object NetworkSimulator: ExternalResource() {
|
||||
throwIf(isNetworkTimeout, SocketTimeoutException("Simulated network timeout"))
|
||||
|
||||
runBlocking {
|
||||
throwIf(connectivity?.first() == NONE, UnknownHostException("Simulated disabled network"))
|
||||
throwIf(connectivity.first() == NONE, UnknownHostException("Simulated disabled network"))
|
||||
}
|
||||
|
||||
it.proceed(it.request())
|
||||
@@ -82,6 +83,21 @@ object NetworkSimulator: ExternalResource() {
|
||||
fun enableNetwork(connectivity: BackupConnectivityManager.Connectivity = UNMETERED) =
|
||||
setConnectivity(connectivity)
|
||||
|
||||
fun disableNetworkFor(duration: Duration, block : () -> Unit) {
|
||||
disableNetwork()
|
||||
|
||||
block()
|
||||
|
||||
val durationMillis = duration.inWholeMilliseconds
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
FusionConfig.Compose.testRule.get().waitUntil((durationMillis * 1.1F).toLong()){
|
||||
System.currentTimeMillis() - start > durationMillis
|
||||
}
|
||||
|
||||
enableNetwork()
|
||||
}
|
||||
|
||||
fun setNetworkTimeout(isNetworkTimeout: Boolean) = apply {
|
||||
NetworkSimulator.isNetworkTimeout = isNetworkTimeout
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -19,11 +19,14 @@
|
||||
package me.proton.android.drive.ui.rules
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.proton.android.drive.repository.TestFeatureFlagRepositoryImpl
|
||||
import me.proton.android.drive.ui.annotation.FeatureFlag
|
||||
import me.proton.android.drive.ui.annotation.Quota
|
||||
import me.proton.android.drive.ui.extension.populate
|
||||
import me.proton.android.drive.ui.extension.quotaSetUsedSpace
|
||||
import me.proton.android.drive.ui.extension.volumeCreate
|
||||
import me.proton.android.drive.ui.test.AbstractBaseTest.Companion.loginTestHelper
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.test.quark.data.User
|
||||
import me.proton.core.test.quark.v2.QuarkCommand
|
||||
import me.proton.core.test.quark.v2.command.seedNewSubscriber
|
||||
@@ -38,37 +41,48 @@ class UserLoginRule(
|
||||
private val isPhotos: Boolean = false,
|
||||
private val quarkCommands: QuarkCommand,
|
||||
) : TestWatcher() {
|
||||
|
||||
var userId: UserId? = null
|
||||
|
||||
override fun starting(description: Description) {
|
||||
runBlocking {
|
||||
loginTestHelper.logoutAll()
|
||||
|
||||
val userPlanAnnotation = description.getAnnotation(UserPlan::class.java)
|
||||
val scenarioAnnotation = description.getAnnotation(Scenario::class.java)
|
||||
val quotaAnnotation = description.getAnnotation(Quota::class.java)
|
||||
val featureFlagAnnotation = description.getAnnotation(FeatureFlag::class.java)
|
||||
|
||||
val scenarioUser = scenarioAnnotation
|
||||
?.let {
|
||||
testUser.copy(dataSetScenario = it.value.toString())
|
||||
} ?: testUser
|
||||
val user = testUser
|
||||
.updateWith(userPlanAnnotation)
|
||||
.updateWith(scenarioAnnotation)
|
||||
|
||||
val device = scenarioAnnotation?.isDevice ?: isDevice
|
||||
val photos = scenarioAnnotation?.isPhotos ?: isPhotos
|
||||
|
||||
if (testUser.isPaid)
|
||||
quarkCommands.seedNewSubscriber(testUser)
|
||||
if (user.isPaid)
|
||||
quarkCommands.seedNewSubscriber(user)
|
||||
else
|
||||
quarkCommands.userCreate(testUser)
|
||||
quarkCommands.userCreate(user).also { response ->
|
||||
userId = response.userId.let(::UserId)
|
||||
}
|
||||
|
||||
quotaAnnotation?.let {
|
||||
quarkCommands.volumeCreate(testUser, "${it.value}${it.unit}")
|
||||
quarkCommands.volumeCreate(user)
|
||||
|
||||
if (it.percentageFull in 1..100) {
|
||||
val usedSpace = (it.value.toDouble() * it.percentageFull / 100).roundToInt()
|
||||
quarkCommands.quotaSetUsedSpace(testUser, "${usedSpace}${it.unit}")
|
||||
quarkCommands.quotaSetUsedSpace(user, "${usedSpace}${it.unit}")
|
||||
}
|
||||
}
|
||||
|
||||
if (scenarioUser.dataSetScenario.let { it.isNotEmpty() && it != "0" }) {
|
||||
quarkCommands.populate(scenarioUser, device, photos)
|
||||
if (user.dataSetScenario.let { it.isNotEmpty() && it != "0" }) {
|
||||
quarkCommands.populate(user, device, photos)
|
||||
}
|
||||
|
||||
if (featureFlagAnnotation != null) {
|
||||
TestFeatureFlagRepositoryImpl.flags[featureFlagAnnotation.id] =
|
||||
featureFlagAnnotation.state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +96,23 @@ class UserLoginRule(
|
||||
}
|
||||
|
||||
override fun finished(description: Description) {
|
||||
TestFeatureFlagRepositoryImpl.flags.clear()
|
||||
loginTestHelper.logoutAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun User.updateWith(
|
||||
userAnnotation: UserPlan?,
|
||||
): User = updateWith(userAnnotation) { annotation ->
|
||||
copy(plan = annotation.value)
|
||||
}
|
||||
|
||||
private fun User.updateWith(
|
||||
scenarioAnnotation: Scenario?,
|
||||
): User = updateWith(scenarioAnnotation) { annotation ->
|
||||
copy(dataSetScenario = annotation.value.toString())
|
||||
}
|
||||
|
||||
private fun <T> User.updateWith(value: T?, block: User.(T) -> User) = value?.let {
|
||||
block(it)
|
||||
} ?: this
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.rules
|
||||
|
||||
import me.proton.core.test.quark.data.Plan
|
||||
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class UserPlan(
|
||||
val value: Plan = Plan.Free,
|
||||
)
|
||||
@@ -20,9 +20,18 @@ package me.proton.android.drive.ui.test
|
||||
|
||||
import me.proton.android.drive.ui.MainActivity
|
||||
import me.proton.android.drive.ui.extension.createFusionAndroidComposeRule
|
||||
import me.proton.core.auth.test.robot.AddAccountRobot
|
||||
import me.proton.core.test.quark.data.User
|
||||
import org.junit.Rule
|
||||
|
||||
abstract class BaseTest : AbstractBaseTest() {
|
||||
@get:Rule(order = 2)
|
||||
val fusionComposeRule = createFusionAndroidComposeRule<MainActivity>()
|
||||
|
||||
@Suppress("unused")
|
||||
protected fun signIn(existingUser: User) {
|
||||
AddAccountRobot
|
||||
.clickSignIn()
|
||||
.login(existingUser)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -19,7 +19,7 @@
|
||||
package me.proton.android.drive.ui.test.flow.account
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.auth.test.robot.AddAccountRobot
|
||||
import me.proton.core.test.quark.v2.command.expireSession
|
||||
@@ -33,7 +33,7 @@ class ForcedSignOutFlowTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
fun forcedSignOut() {
|
||||
FilesTabRobot.verify {
|
||||
PhotosTabRobot.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -32,6 +32,10 @@ class HomeFlowTest : AuthenticatedBaseTest() {
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickPhotosTab()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickSharedTab()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -20,7 +20,7 @@ package me.proton.android.drive.ui.test.flow.account
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.report.test.MinimalReportInternalTests
|
||||
import me.proton.core.test.quark.Quark
|
||||
@@ -47,17 +47,18 @@ class ReportFlowTest : BaseTest(), MinimalReportInternalTests {
|
||||
}
|
||||
|
||||
override fun verifyBefore() {
|
||||
FilesTabRobot.verify { homeScreenDisplayed() }
|
||||
PhotosTabRobot
|
||||
.verify { homeScreenDisplayed() }
|
||||
}
|
||||
|
||||
override fun startReport() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.openSidebarBySwipe()
|
||||
.verify { robotDisplayed() }
|
||||
.clickReportBug()
|
||||
}
|
||||
|
||||
override fun verifyAfter() {
|
||||
FilesTabRobot.verify { homeScreenDisplayed() }
|
||||
PhotosTabRobot.verify { homeScreenDisplayed() }
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.test.flow.computers
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class EmptyComputersTabTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
fun emptyComputers() {
|
||||
FilesTabRobot
|
||||
.clickComputersTab()
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
assertEmptyComputers()
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.test.flow.computers
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.RenameRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.android.drive.utils.getRandomString
|
||||
import me.proton.core.test.android.instrumented.utils.StringUtils
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltAndroidTest
|
||||
@RunWith(Parameterized::class)
|
||||
class RenamingComputerErrorFlowTest(
|
||||
private val computerToBeRenamed: String,
|
||||
private val newComputerName: String,
|
||||
private val errorMessage: String,
|
||||
@Suppress("unused") private val friendlyName: String,
|
||||
) : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun renameComputerWithInvalidName() {
|
||||
FilesTabRobot
|
||||
.clickComputersTab()
|
||||
.scrollToComputer(computerToBeRenamed)
|
||||
.clickMoreOnComputer(computerToBeRenamed)
|
||||
.clickRename()
|
||||
.clearName()
|
||||
.typeName(newComputerName)
|
||||
.clickRename(RenameRobot)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MY_DEVICE_1 = "MyDevice1"
|
||||
@get:Parameterized.Parameters(name = "{3}")
|
||||
@get:JvmStatic
|
||||
val data = listOf(
|
||||
arrayOf(
|
||||
MY_DEVICE_1,
|
||||
"",
|
||||
StringUtils.stringFromResource(I18N.string.common_error_name_is_blank),
|
||||
"Empty device name"
|
||||
),
|
||||
arrayOf(
|
||||
MY_DEVICE_1,
|
||||
" \t ",
|
||||
StringUtils.stringFromResource(I18N.string.common_error_name_is_blank),
|
||||
"Blank device name"
|
||||
),
|
||||
arrayOf(
|
||||
MY_DEVICE_1,
|
||||
getRandomString(256),
|
||||
StringUtils.stringFromResource(I18N.string.common_error_name_too_long, 255),
|
||||
"Too long (256) name"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.test.flow.computers
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.ComputersTabRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class RenamingComputerSuccessFlowTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun renameComputer() {
|
||||
val computerToBeRenamed = MY_DEVICE_1
|
||||
val newComputerName = "My device X"
|
||||
FilesTabRobot
|
||||
.clickComputersTab()
|
||||
.scrollToComputer(computerToBeRenamed)
|
||||
.clickMoreOnComputer(computerToBeRenamed)
|
||||
.clickRename()
|
||||
.clearName()
|
||||
.typeName(newComputerName)
|
||||
.clickRename(ComputersTabRobot)
|
||||
.verify {
|
||||
itemIsDisplayed(newComputerName)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MY_DEVICE_1 = "MyDevice1"
|
||||
}
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.test.flow.computers
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Instrumentation
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.intent.rule.IntentsRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.ComputersTabRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.MoveToFolderRobot
|
||||
import me.proton.android.drive.ui.robot.ShareRobot
|
||||
import me.proton.android.drive.ui.rules.ExternalFilesRule
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncedFoldersFlowTest : AuthenticatedBaseTest() {
|
||||
|
||||
@get:Rule
|
||||
val intentsTestRule = IntentsRule()
|
||||
|
||||
@get:Rule
|
||||
val externalFilesRule = ExternalFilesRule()
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun emptySyncedFolders() {
|
||||
FilesTabRobot
|
||||
.clickComputersTab()
|
||||
.scrollToComputer(MY_DEVICE_3)
|
||||
.clickOnComputer(MY_DEVICE_3)
|
||||
.verify {
|
||||
assertEmptySyncedFolders()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun uploadAFile() {
|
||||
val fileName = "1kB.txt"
|
||||
val file = externalFilesRule.createFile(fileName, 1000.bytes.value)
|
||||
|
||||
Intents.intending(IntentMatchers.hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWithFunction {
|
||||
Instrumentation.ActivityResult(Activity.RESULT_OK, Intent().setData(Uri.fromFile(file)))
|
||||
}
|
||||
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.clickPlusButton()
|
||||
.clickUploadAFile()
|
||||
.verify {
|
||||
itemIsDisplayed(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun createAFolderViaPlusButton() {
|
||||
val newFolderName = "TestFolder"
|
||||
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.clickPlusButton()
|
||||
.clickCreateFolder()
|
||||
.typeFolderName(newFolderName)
|
||||
.clickCreate(FilesTabRobot)
|
||||
.dismissFolderCreateSuccessGrowler(newFolderName, FilesTabRobot)
|
||||
.verify {
|
||||
itemIsDisplayed(newFolderName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun moveFile() {
|
||||
val file = "file3"
|
||||
val folder = "folder"
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER_3)
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMove()
|
||||
.scrollToItemWithName(folder)
|
||||
.clickOnFolderToMove(folder)
|
||||
.clickMoveToFolder(folder)
|
||||
|
||||
ComputersTabRobot
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
.verify {
|
||||
itemIsNotDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun moveFileToAnotherSyncedFolder() {
|
||||
val file = "file1"
|
||||
val folder = MY_DEVICE_1_SYNC_FOLDER_3
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMove()
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
.clickBack(MoveToFolderRobot)
|
||||
.verify {
|
||||
assertMoveButtonIsDisabled()
|
||||
}
|
||||
.scrollToItemWithName(folder)
|
||||
.clickOnFolderToMove(folder)
|
||||
.clickMoveToFolder(folder)
|
||||
|
||||
ComputersTabRobot
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
}
|
||||
.clickBack(ComputersTabRobot)
|
||||
.clickOnFolder(folder)
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun renameFile() {
|
||||
val itemToBeRenamed = "presentation.pdf"
|
||||
val newItemName = "Dummy_PDF_file.pdf"
|
||||
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.scrollToItemWithName(itemToBeRenamed)
|
||||
.clickMoreOnItem(itemToBeRenamed)
|
||||
.clickRename()
|
||||
.clearName()
|
||||
.typeName(newItemName)
|
||||
.clickRename()
|
||||
.scrollToItemWithName(newItemName)
|
||||
.verify {
|
||||
itemIsDisplayed(newItemName)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun shareFile() {
|
||||
val file = "picWithThumbnail.jpg"
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.clickMoreOnItem(file)
|
||||
.clickGetLink()
|
||||
.verifyShareLinkFile(file)
|
||||
|
||||
ShareRobot
|
||||
.clickBack(ComputersTabRobot)
|
||||
|
||||
FilesTabRobot
|
||||
.clickSharedTab()
|
||||
.scrollToItemWithName(file)
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun moveFileToTrash() {
|
||||
val file = "picWithThumbnail.jpg"
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMoveToTrash()
|
||||
.dismissMoveToTrashSuccessGrowler(1, FilesTabRobot)
|
||||
.verify {
|
||||
itemIsNotDisplayed(file)
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickTrash()
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun restoreFromTrash() {
|
||||
val file = "trashedFileInDevice"
|
||||
FilesTabRobot
|
||||
.clickSidebarButton()
|
||||
.clickTrash()
|
||||
.clickMoreOnItem(file)
|
||||
.clickRestoreTrash()
|
||||
.clickBack(FilesTabRobot)
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun makeAvailableOffline() {
|
||||
val file = "presentation.pdf"
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(value = 2, isDevice = true)
|
||||
fun previewPdfFile() {
|
||||
val file = "presentation.pdf"
|
||||
FilesTabRobot
|
||||
.navigateToComputerSyncedFolder(MY_DEVICE_1, MY_DEVICE_1_SYNC_FOLDER)
|
||||
.scrollToItemWithName(file)
|
||||
.clickOnFile(file)
|
||||
.verify {
|
||||
nodeWithTextDisplayed("1 / 1")
|
||||
}
|
||||
}
|
||||
|
||||
private fun FilesTabRobot.navigateToComputerSyncedFolder(
|
||||
computerName: String,
|
||||
syncedFolderName: String,
|
||||
): FilesTabRobot =
|
||||
this
|
||||
.clickComputersTab()
|
||||
.scrollToComputer(computerName)
|
||||
.clickOnComputer(computerName)
|
||||
.clickOnFolder(syncedFolderName)
|
||||
|
||||
companion object {
|
||||
private const val MY_DEVICE_1 = "MyDevice1"
|
||||
private const val MY_DEVICE_1_SYNC_FOLDER = "syncFolder"
|
||||
private const val MY_DEVICE_1_SYNC_FOLDER_3 = "syncFolder3"
|
||||
private const val MY_DEVICE_3 = "MyDevice3"
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -21,6 +21,7 @@ package me.proton.android.drive.ui.test.flow.creatingFolder
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.CreateFolderRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.android.drive.utils.getRandomString
|
||||
import org.junit.Test
|
||||
@@ -32,7 +33,8 @@ class CreatingFolderEmptyFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
fun createAFolderViaPlusButton() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickPlusButton()
|
||||
.clickCreateFolder()
|
||||
|
||||
@@ -42,7 +44,8 @@ class CreatingFolderEmptyFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
@Test
|
||||
fun createChildFolderViaCTAButton() {
|
||||
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickAddFilesButton()
|
||||
.clickCreateFolder()
|
||||
|
||||
|
||||
+3
-1
@@ -19,6 +19,7 @@ package me.proton.android.drive.ui.test.flow.creatingFolder
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.android.drive.utils.getRandomString
|
||||
@@ -39,7 +40,8 @@ class CreatingFolderFlowErrorTest(
|
||||
@Test
|
||||
@Scenario(2)
|
||||
fun createFolderError() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickPlusButton()
|
||||
.clickCreateFolder()
|
||||
.typeFolderName(folderName)
|
||||
|
||||
+7
-3
@@ -22,6 +22,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.CreateFolderRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.MoveToFolderRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.android.drive.ui.test.SmokeTest
|
||||
@@ -40,7 +41,8 @@ class CreatingFolderFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
val subFolderName = "folder1"
|
||||
val newFolderName = getRandomString()
|
||||
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(subFolderName)
|
||||
.clickMoreOnItem(subFolderName)
|
||||
.clickMove()
|
||||
@@ -57,7 +59,8 @@ class CreatingFolderFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
@Test
|
||||
@Scenario(2)
|
||||
fun createFolderInGridLayout() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickLayoutSwitcher()
|
||||
.clickPlusButton()
|
||||
.clickCreateFolder()
|
||||
@@ -70,7 +73,8 @@ class CreatingFolderFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
fun createAFolderViaSubFolderPlusButton() {
|
||||
val subFolderName = "folder1"
|
||||
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(subFolderName)
|
||||
.clickPlusButton()
|
||||
.clickCreateFolder()
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.test.flow.deeplink
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.navigation.Screen
|
||||
import me.proton.android.drive.ui.robot.ComputersTabRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.LauncherRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.SharedTabRobot
|
||||
import me.proton.android.drive.ui.robot.StorageFullRobot
|
||||
import me.proton.android.drive.ui.rules.UserLoginRule
|
||||
import me.proton.android.drive.ui.test.EmptyBaseTest
|
||||
import me.proton.android.drive.utils.getRandomString
|
||||
import me.proton.core.test.quark.data.User
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class DeeplinkFlowTest : EmptyBaseTest() {
|
||||
|
||||
private val testUser = User(name = "proton_drive_${getRandomString(15)}")
|
||||
|
||||
@get:Rule(order = 1)
|
||||
val userLoginRule: UserLoginRule =
|
||||
UserLoginRule(testUser, quarkCommands = quarkRule.quarkCommands)
|
||||
|
||||
@Test
|
||||
fun launchFiles() {
|
||||
LauncherRobot.launch("files", FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeFiles() {
|
||||
LauncherRobot.deeplinkTo(Screen.Files(userLoginRule.userId!!), FilesTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun launchPhotos() {
|
||||
LauncherRobot.launch("photos", PhotosTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homePhotos() {
|
||||
LauncherRobot.deeplinkTo(Screen.Photos(userLoginRule.userId!!), PhotosTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun launchComputers() {
|
||||
LauncherRobot.launch("computers", ComputersTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeComputers() {
|
||||
LauncherRobot.deeplinkTo(Screen.Computers(userLoginRule.userId!!), ComputersTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun launchShared() {
|
||||
LauncherRobot.launch("shared", SharedTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
@Test
|
||||
fun homeShared() {
|
||||
LauncherRobot.deeplinkTo(Screen.Shared(userLoginRule.userId!!), SharedTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun storageFull() {
|
||||
LauncherRobot.deeplinkTo(
|
||||
Screen.Dialogs.StorageFull(userLoginRule.userId!!),
|
||||
StorageFullRobot
|
||||
)
|
||||
.verify { robotDisplayed() }
|
||||
.clickDismiss(PhotosTabRobot)
|
||||
.verify { robotDisplayed() }
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -21,6 +21,7 @@ package me.proton.android.drive.ui.test.flow.details
|
||||
import android.os.Build
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
@@ -36,7 +37,8 @@ class DetailsFlowTest : AuthenticatedBaseTest() {
|
||||
@Test
|
||||
@Scenario(4)
|
||||
fun checkFileDetailsAndCloseDetails() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(sharedImage.name)
|
||||
.clickMoreOnItem(sharedImage.name)
|
||||
@@ -85,7 +87,8 @@ class DetailsFlowTest : AuthenticatedBaseTest() {
|
||||
@Test
|
||||
@Scenario(4)
|
||||
fun checkFolderDetailsAndCloseDetails() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.verify { robotDisplayed() }
|
||||
.scrollToItemWithName(sharedFolder.name)
|
||||
.clickMoreOnItem(sharedFolder.name)
|
||||
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -20,6 +20,7 @@ package me.proton.android.drive.ui.test.flow.move
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
@@ -34,7 +35,8 @@ class MoveFileDeepFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
val file = "file4"
|
||||
val folder2 = "folder2"
|
||||
val folder5 = "folder5"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(folder2)
|
||||
.clickOnFolder(folder5)
|
||||
.clickMoreOnItem(file)
|
||||
@@ -57,7 +59,8 @@ class MoveFileDeepFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
val folder1 = "folder1"
|
||||
val folder2 = "folder2"
|
||||
val folder5 = "folder5"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(folder2)
|
||||
.clickOnFolder(folder5)
|
||||
.clickMoreOnItem(file)
|
||||
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -19,7 +19,7 @@
|
||||
package me.proton.android.drive.ui.test.flow.move
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
@@ -33,7 +33,8 @@ class MoveFileFlowErrorTest: AuthenticatedBaseTest() {
|
||||
val file = "sharedChild.html"
|
||||
val folderSource = "expiredSharedFolder"
|
||||
val folderDestination = "sharedFolder"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(folderSource)
|
||||
.clickOnFolder(folderSource)
|
||||
.verify {
|
||||
|
||||
+38
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -20,9 +20,11 @@ package me.proton.android.drive.ui.test.flow.move
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltAndroidTest
|
||||
@@ -32,7 +34,8 @@ class MoveFileFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
fun moveAFileToRoot() {
|
||||
val file = "sharedChild.html"
|
||||
val folder = "sharedFolder"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(folder)
|
||||
.clickOnFolder(folder)
|
||||
.scrollToItemWithName(file)
|
||||
@@ -56,7 +59,8 @@ class MoveFileFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
fun moveRootFileToFolder() {
|
||||
val file = "shared.jpg"
|
||||
val folder = "folder3"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMove()
|
||||
@@ -79,7 +83,8 @@ class MoveFileFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
fun moveFilesFromRootFolderToAnotherFolder() {
|
||||
val file = "shared.jpg"
|
||||
val folder = "folder3"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.longClickOnItem(file)
|
||||
.clickOptions()
|
||||
@@ -91,4 +96,33 @@ class MoveFileFlowSuccessTest : AuthenticatedBaseTest() {
|
||||
}
|
||||
// TODO: Verify that the file is really moved when DRVAND-449 is fixed
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(4)
|
||||
// This test might be flaky due to snackbar not showing
|
||||
fun undoMoveRootFileToFolder() {
|
||||
val file = "shared.jpg"
|
||||
val folder = "folder3"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMove()
|
||||
.scrollToItemWithName(folder)
|
||||
.clickOnFolderToMove(folder)
|
||||
.clickMoveToFolder(folder)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(I18N.string.file_operation_moving_file_successful)
|
||||
}
|
||||
// Server sometimes responds with 422 "This file or folder was out of date, move failed."
|
||||
// if undo is called too quickly, thus a 2 seconds delay before clicking on Undo
|
||||
.clickOnUndo(after = 2.seconds, FilesTabRobot)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(I18N.string.file_operation_moving_file_successful)
|
||||
}
|
||||
.scrollToItemWithName(file)
|
||||
.verify {
|
||||
itemIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -22,6 +22,7 @@ import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.annotation.Slow
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.MoveToFolderRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
@@ -36,7 +37,7 @@ class MoveFolderFlowTest : AuthenticatedBaseTest() {
|
||||
fun moveAFolderToRoot() {
|
||||
val parent = "folder1"
|
||||
val folder = "folder3"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickOnFolder(parent)
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMove()
|
||||
@@ -57,7 +58,8 @@ class MoveFolderFlowTest : AuthenticatedBaseTest() {
|
||||
fun moveRootFolderToFolder() {
|
||||
val folder = "folder1"
|
||||
val folderDestination = "folder2"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMove()
|
||||
.clickOnFolderToMove(folderDestination)
|
||||
@@ -78,7 +80,8 @@ class MoveFolderFlowTest : AuthenticatedBaseTest() {
|
||||
val parent = "folder1"
|
||||
val folder = "folder3"
|
||||
val folderDestination = "folder2"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(parent)
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMove()
|
||||
@@ -102,7 +105,8 @@ class MoveFolderFlowTest : AuthenticatedBaseTest() {
|
||||
@Scenario(2)
|
||||
fun cannotMoveAFolderToSubFolderOfItself() {
|
||||
val folder = "folder1"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMove()
|
||||
.clickOnFolder(folder)
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2024 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.test.flow.move
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.drive.files.presentation.extension.LayoutType
|
||||
import org.junit.Test
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltAndroidTest
|
||||
class MoveFolderGridFlowTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
@Scenario(1)
|
||||
fun moveAFolderInGrid() {
|
||||
val folder = "folder1"
|
||||
val folderDestination = "folder2"
|
||||
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickLayoutSwitcher()
|
||||
.scrollToItemWithName(folder)
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMove()
|
||||
.clickOnFolderToMove(folderDestination)
|
||||
.clickMoveToFolder(folderDestination)
|
||||
.verify {
|
||||
nodeWithTextDisplayed(I18N.string.file_operation_moving_folder_successful)
|
||||
robotDisplayed()
|
||||
}
|
||||
.scrollToItemWithName(folderDestination)
|
||||
.clickOnFolder(folderDestination, LayoutType.Grid)
|
||||
.verify {
|
||||
itemIsDisplayed(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 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.test.flow.offline
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.NetworkSimulator
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class AvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
fun emptyAvailableOfflineScreen() {
|
||||
PhotosTabRobot
|
||||
.openSidebarBySwipe()
|
||||
.clickOffline()
|
||||
.verify {
|
||||
assertIsEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(2)
|
||||
fun aChildOfADeletedFolderShouldNotBeDisplayedInAvailableOffline() {
|
||||
val folder = "folder1"
|
||||
val file = "file2"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(folder)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
.clickBack(FilesTabRobot)
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMoveToTrash()
|
||||
.dismissMoveToTrashSuccessGrowler(1, FilesTabRobot)
|
||||
.openSidebarBySwipe()
|
||||
.clickOffline()
|
||||
.verify {
|
||||
itemIsNotDisplayed(file)
|
||||
assertIsEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(2)
|
||||
fun previewItemsWithoutConnection() {
|
||||
val file = "image.jpg"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
|
||||
NetworkSimulator.disableNetwork()
|
||||
|
||||
FilesTabRobot
|
||||
.clickOnFile(file)
|
||||
.verify {
|
||||
assertPreviewIsDisplayed(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(2)
|
||||
fun newlyAddedFilesToAFolderShouldBeDownloadedInstantly() {
|
||||
|
||||
val file = "image.jpg"
|
||||
val folder = "folder1"
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
// Wait for folder to be downloaded before after adding a new file
|
||||
// else it will be ignored
|
||||
itemIsDisplayed(folder, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
.clickMoreOnItem(file)
|
||||
.clickMove()
|
||||
.clickOnFolderToMove(folder)
|
||||
.clickMoveToFolder(folder)
|
||||
.clickOnFolder(folder)
|
||||
.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -20,12 +20,15 @@ package me.proton.android.drive.ui.test.flow.offline
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.data.ImageName
|
||||
import me.proton.android.drive.ui.robot.FileFolderOptionsRobot
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.NetworkSimulator
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
import org.junit.Test
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@HiltAndroidTest
|
||||
class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
@@ -34,7 +37,8 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
fun makeFileAvailableOffline() {
|
||||
val folder = "folder1"
|
||||
val file = "presentation.pdf"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(folder)
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
@@ -54,7 +58,8 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
fun makeFolderWithChildFolderAvailableOffline() {
|
||||
val folder = "folder2"
|
||||
val subfolder = "folder5"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
@@ -76,7 +81,8 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
@Scenario(4)
|
||||
fun makeEmptyFolderAvailableOffline() {
|
||||
val folder = "folder1"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
@@ -95,8 +101,7 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
val firstImage = ImageName.Yesterday
|
||||
val secondImage = ImageName.Now
|
||||
|
||||
FilesTabRobot
|
||||
.clickPhotosTab()
|
||||
PhotosTabRobot
|
||||
.verify {
|
||||
assertPhotoDisplayed(firstImage)
|
||||
assertPhotoDisplayed(secondImage)
|
||||
@@ -124,7 +129,8 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
fun makeFolderWithFileInsideAvailableOffline() {
|
||||
val folder = "sharedFolder"
|
||||
val file = "sharedChild.html"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
@@ -151,7 +157,8 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
fun makeFolderAndChildFileAvailableOfflineSeparately() {
|
||||
val folder = "sharedFolder"
|
||||
val file = "sharedChild.html"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.clickOnFolder(folder)
|
||||
@@ -165,4 +172,24 @@ class MakeAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Scenario(2)
|
||||
fun loseConnectionWhenDownloadingIsStarted() {
|
||||
|
||||
val file = "image.jpg"
|
||||
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(file)
|
||||
|
||||
NetworkSimulator.disableNetworkFor(5.seconds) {
|
||||
FileFolderOptionsRobot
|
||||
.clickMakeAvailableOffline()
|
||||
}
|
||||
|
||||
FilesTabRobot.verify {
|
||||
itemIsDisplayed(file, downloadState = SemanticsDownloadState.Downloaded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -35,7 +35,8 @@ class RemoveAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
@Scenario(4)
|
||||
fun removeFileFromAvailableOfflineFromMyFile() {
|
||||
val file = "shared.jpg"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
@@ -53,7 +54,8 @@ class RemoveAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
@Scenario(4)
|
||||
fun removeFileFromAvailableOfflineFromOffline() {
|
||||
val file = "shared.jpg"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
.clickMakeAvailableOffline()
|
||||
@@ -74,7 +76,8 @@ class RemoveAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
@Scenario(4)
|
||||
fun removeFolderFromAvailableOffline() {
|
||||
val folder = "folder1"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.verify {
|
||||
@@ -99,7 +102,8 @@ class RemoveAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
fun separatelyAddedFileAreAvailableOfflineAfterParentIsRemoved() {
|
||||
val folder = "sharedFolder"
|
||||
val file = "sharedChild.html"
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickMoreOnItem(folder)
|
||||
.clickMakeAvailableOffline()
|
||||
.clickOnFolder(folder)
|
||||
@@ -132,8 +136,7 @@ class RemoveAvailableOfflineFlowTest : AuthenticatedBaseTest() {
|
||||
fun removePhotoFromAvailableOffline() {
|
||||
val image = ImageName.Now
|
||||
|
||||
FilesTabRobot
|
||||
.clickPhotosTab()
|
||||
PhotosTabRobot
|
||||
.longClickOnPhoto(image.fileName)
|
||||
.clickOptions()
|
||||
.clickMakeAvailableOffline()
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -20,6 +20,7 @@ package me.proton.android.drive.ui.test.flow.offline
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.Scenario
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import me.proton.core.drive.files.presentation.extension.SemanticsDownloadState
|
||||
@@ -33,7 +34,8 @@ class RemoveAvailableOfflineLargeFileFlowTest : AuthenticatedBaseTest() {
|
||||
val folder = "folder1"
|
||||
val file = "40mbfile"
|
||||
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.clickFilesTab()
|
||||
.clickOnFolder(folder)
|
||||
.scrollToItemWithName(file)
|
||||
.clickMoreOnItem(file)
|
||||
|
||||
+1
-3
@@ -19,7 +19,6 @@
|
||||
package me.proton.android.drive.ui.test.flow.photos
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.rules.NetworkSimulator
|
||||
import me.proton.android.drive.ui.test.PhotosBaseTest
|
||||
@@ -32,8 +31,7 @@ class NoConnectivityTest: PhotosBaseTest() {
|
||||
@Before
|
||||
fun prepare() {
|
||||
pictureCameraFolder.copyDirFromAssets("images/basic")
|
||||
FilesTabRobot
|
||||
.clickPhotosTab()
|
||||
PhotosTabRobot
|
||||
.verify {
|
||||
robotDisplayed()
|
||||
assertEnableBackupDisplayed()
|
||||
|
||||
+8
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
@@ -19,10 +19,8 @@
|
||||
package me.proton.android.drive.ui.test.flow.photos
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import me.proton.android.drive.ui.robot.FilesTabRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosNoPermissionsRobot
|
||||
import me.proton.android.drive.ui.robot.PhotosTabRobot
|
||||
import me.proton.android.drive.ui.robot.SettingsRobot
|
||||
import me.proton.android.drive.ui.robot.settings.PhotosBackupRobot
|
||||
import me.proton.android.drive.ui.test.AuthenticatedBaseTest
|
||||
import org.junit.Test
|
||||
@@ -31,8 +29,7 @@ import org.junit.Test
|
||||
class NoPermissionsTest : AuthenticatedBaseTest() {
|
||||
@Test
|
||||
fun denyPermissionsFromPhotoTab() {
|
||||
FilesTabRobot
|
||||
.clickPhotosTab()
|
||||
PhotosTabRobot
|
||||
.enableBackup()
|
||||
|
||||
PhotosNoPermissionsRobot
|
||||
@@ -48,7 +45,11 @@ class NoPermissionsTest : AuthenticatedBaseTest() {
|
||||
|
||||
@Test
|
||||
fun denyPermissionsFromSettings() {
|
||||
FilesTabRobot
|
||||
PhotosTabRobot
|
||||
.verify {
|
||||
// Wait for photo share to be created to avoid collision
|
||||
assertEnableBackupDisplayed()
|
||||
}
|
||||
.openSidebarBySwipe()
|
||||
.clickSettings()
|
||||
.clickPhotosBackup()
|
||||
@@ -62,4 +63,4 @@ class NoPermissionsTest : AuthenticatedBaseTest() {
|
||||
robotDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user