/* * SPDX-FileCopyrightText: 2023 The Calyx Institute * SPDX-License-Identifier: Apache-2.0 */ package com.stevesoltys.seedvault.e2e import android.app.UiAutomation import android.app.backup.IBackupManager import android.content.Context import android.content.pm.PackageInfo import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Environment import androidx.annotation.WorkerThread import androidx.preference.PreferenceManager import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.stevesoltys.seedvault.crypto.ANDROID_KEY_STORE import com.stevesoltys.seedvault.crypto.KEY_ALIAS_BACKUP import com.stevesoltys.seedvault.crypto.KEY_ALIAS_MAIN import com.stevesoltys.seedvault.crypto.KeyManager import com.stevesoltys.seedvault.currentRestoreStorageViewModel import com.stevesoltys.seedvault.currentRestoreViewModel import com.stevesoltys.seedvault.e2e.screen.impl.BackupScreen import com.stevesoltys.seedvault.e2e.screen.impl.DocumentPickerScreen import com.stevesoltys.seedvault.e2e.screen.impl.RecoveryCodeScreen import com.stevesoltys.seedvault.metadata.MetadataManager import com.stevesoltys.seedvault.permitDiskReads import com.stevesoltys.seedvault.restore.RestoreViewModel import com.stevesoltys.seedvault.settings.SettingsManager import com.stevesoltys.seedvault.transport.backup.PackageService import com.stevesoltys.seedvault.ui.storage.RestoreStorageViewModel import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.get import java.io.File import java.lang.Thread.sleep import java.security.KeyStore import java.security.MessageDigest import java.text.SimpleDateFormat import java.util.Calendar import java.util.concurrent.atomic.AtomicBoolean internal interface LargeTestBase : KoinComponent { companion object { private const val TEST_STORAGE_FOLDER = "seedvault_test" private const val TEST_RESULT_FOLDER = "seedvault_test_results" } val externalStorageDir: String get() = Environment.getExternalStorageDirectory().absolutePath val testStoragePath get() = "$externalStorageDir/$TEST_STORAGE_FOLDER" val testResultPath get() = "$externalStorageDir/$TEST_RESULT_FOLDER" val targetContext: Context get() = InstrumentationRegistry.getInstrumentation().targetContext val uiAutomation: UiAutomation get() = InstrumentationRegistry.getInstrumentation().uiAutomation val device: UiDevice get() = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val packageService: PackageService get() = get() val settingsManager: SettingsManager get() = get() val keyManager: KeyManager get() = get() val spyMetadataManager: MetadataManager get() = get() val backupManager: IBackupManager get() = get() val spyRestoreViewModel: RestoreViewModel get() = currentRestoreViewModel ?: error("currentRestoreViewModel is null") val spyRestoreStorageViewModel: RestoreStorageViewModel get() = currentRestoreStorageViewModel ?: error("currentRestoreStorageViewModel is null") fun resetApplicationState() { backupManager.setAutoRestore(false) val sharedPreferences = permitDiskReads { PreferenceManager.getDefaultSharedPreferences(targetContext) } sharedPreferences.edit().clear().apply() KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) }.apply { deleteEntry(KEY_ALIAS_MAIN) deleteEntry(KEY_ALIAS_BACKUP) } clearDocumentPickerAppData() device.executeShellCommand("rm -R $externalStorageDir/.SeedVaultAndroidBackup") } fun waitUntilIdle() { device.waitForIdle() sleep(3000) } fun runCommand(command: String) { uiAutomation.executeShellCommand(command).close() } fun testResultFilename(testName: String): String { val simpleDateFormat = SimpleDateFormat("yyyyMMdd_hhmmss") val timeStamp = simpleDateFormat.format(Calendar.getInstance().time) return "${timeStamp}_${testName.replace(" ", "_")}" } @OptIn(DelicateCoroutinesApi::class) @WorkerThread suspend fun startRecordingTest( keepRecordingScreen: AtomicBoolean, testName: String, ) { val folder = testResultPath runCommand("mkdir -p $folder") val fileName = testResultFilename(testName) // screen record automatically stops after 3 minutes // we need to block on a loop and split it into multiple files GlobalScope.launch(Dispatchers.IO) { var index = 0 while (keepRecordingScreen.get()) { device.executeShellCommand("screenrecord $folder/$fileName-${index++}.mp4") } } } @WorkerThread fun stopRecordingTest( keepRecordingScreen: AtomicBoolean, testName: String, ) { keepRecordingScreen.set(false) runCommand("pkill -2 screenrecord") // write logcat to file val fileName = testResultFilename(testName) runCommand("logcat -d -f $testResultPath/$fileName.log") } fun uninstallPackages(packages: Collection) { packages.forEach { runCommand("pm uninstall ${it.packageName}") } } fun clearDocumentPickerAppData() { runCommand("pm clear com.google.android.documentsui") } fun clearTestBackups() { File(testStoragePath).deleteRecursively() File(testResultPath).deleteRecursively() } fun changeBackupLocation( folderName: String = TEST_STORAGE_FOLDER, exists: Boolean = false, ) { BackupScreen { clearDocumentPickerAppData() backupLocationButton.clickAndWaitForNewWindow() chooseStorageLocation(folderName, exists) } } fun chooseStorageLocation( folderName: String = TEST_STORAGE_FOLDER, exists: Boolean = false, ) { val manageDocumentsPermission = targetContext.checkSelfPermission("android.permission.MANAGE_DOCUMENTS") if (manageDocumentsPermission != PERMISSION_GRANTED) { DocumentPickerScreen { if (exists) { existingFolder(folderName).scrollTo().clickAndWaitForNewWindow() } else { createNewFolderButton.clickAndWaitForNewWindow() textBox.setText(folderName) okButton.clickAndWaitForNewWindow() } useThisFolderButton.clickAndWaitForNewWindow() allowButton.clickAndWaitForNewWindow() } } else { val extDir = externalStorageDir device.executeShellCommand("rm -R $extDir/.SeedVaultAndroidBackup") device.executeShellCommand( "cp -R $extDir/$folderName/" + ".SeedVaultAndroidBackup $extDir" ) device.executeShellCommand("cp -R $extDir/$folderName/recovery-code.txt $extDir") BackupScreen { internalStorageButton.clickAndWaitForNewWindow() if (useAnywayButton.waitForExists(3000)) { useAnywayButton.clickAndWaitForNewWindow() } } } BackupScreen { device.wait(Until.hasObject(initializingText), 10000) device.wait(Until.gone(initializingText), 120000) } } fun confirmCode() { RecoveryCodeScreen { startNewBackupButton.click() confirmCodeButton.click() verifyCodeButton.scrollTo().click() } } fun ByteArray.sha256(): String { val data = MessageDigest.getInstance("SHA-256").digest(this) return data.joinToString("") { "%02x".format(it) } } }