Merge branch 'android13' into patch-2

This commit is contained in:
Allan Nordhøy 2022-11-18 17:42:59 +00:00 committed by GitHub
commit f54b5448d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 309 additions and 259 deletions

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Instrumentation tests: app" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests" singleton="true">
<module name="seedvault.app" />
<module name="seedvault.app.androidTest" />
<option name="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" />
@ -8,10 +8,12 @@
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" />
<option name="RETENTION_ENABLED" value="No" />
<option name="RETENTION_MAX_SNAPSHOTS" value="2" />
<option name="RETENTION_COMPRESS_SNAPSHOTS" value="false" />
<option name="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEBUGGER_TYPE" value="Auto" />
<Auto>
@ -40,7 +42,7 @@
<option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Sample Java Methods" />
<option name="STARTUP_CPU_PROFILING_CONFIGURATION_NAME" value="Callstack Sample" />
<option name="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers>
@ -48,4 +50,4 @@
<option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method>
</configuration>
</component>
</component>

View file

@ -26,12 +26,16 @@ android_app {
static_libs: [
"kotlin-stdlib-jdk8",
"androidx.core_core-ktx",
"androidx.fragment_fragment-ktx",
"androidx.activity_activity-ktx",
"androidx.preference_preference",
"androidx.documentfile_documentfile",
"androidx.lifecycle_lifecycle-viewmodel-ktx",
"androidx.lifecycle_lifecycle-livedata-ktx",
"androidx-constraintlayout_constraintlayout",
"com.google.android.material_material",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
// storage backup lib
"seedvault-lib-storage",
// koin
@ -48,8 +52,9 @@ android_app {
privileged: true,
required: [
"LocalContactsBackup",
"privapp_whitelist_com.stevesoltys.backup",
"com.stevesoltys.backup_whitelist"
"com.stevesoltys.backup_allowlist",
"com.stevesoltys.backup_default-permissions",
"com.stevesoltys.backup_privapp_allowlist"
],
optimize: {
enabled: false,
@ -57,17 +62,25 @@ android_app {
}
prebuilt_etc {
name: "privapp_whitelist_com.stevesoltys.backup",
name: "com.stevesoltys.backup_allowlist",
system_ext_specific: true,
sub_dir: "sysconfig",
src: "allowlist_com.stevesoltys.seedvault.xml",
filename_from_src: true,
}
prebuilt_etc {
name: "com.stevesoltys.backup_default-permissions",
system_ext_specific: true,
sub_dir: "default-permissions",
src: "default-permissions_com.stevesoltys.seedvault.xml",
filename_from_src: true,
}
prebuilt_etc {
name: "com.stevesoltys.backup_privapp_allowlist",
system_ext_specific: true,
sub_dir: "permissions",
src: "permissions_com.stevesoltys.seedvault.xml",
filename_from_src: true,
}
prebuilt_etc {
name: "com.stevesoltys.backup_whitelist",
system_ext_specific: true,
sub_dir: "sysconfig",
src: "whitelist_com.stevesoltys.seedvault.xml",
filename_from_src: true,
}

View file

@ -1,3 +1,9 @@
## [13-3.1] - 2022-09-01
* Initial release for Android 13
* Don't attempt to restore app that is used as a backup location (e.g. Nextcloud),
because can cause restore to abort early
* Upgrade several libraries
## [12-3.0] - 2021-10-13
* Initial release for Android 12
* Use the same (faster and more secure) crypto that storage backups use,

View file

@ -19,10 +19,19 @@ If you are having an issue/question, please look at our [FAQ](../../wiki/FAQ).
## Requirements
- Android 12
SeedVault is developed alongwith AOSP releases
We update it every time Google releases a new Android version, make any changes required for basic functionality, and any improvements possible through API changes in the OS.
This means that for ROMs using SeedVault it's recommended to use the same branch as your android version
- This current branch `android13` is meant for usage with Android 13
- This is indicated by the version name starting with `13`, and the version code starting with `33` - the Android 13 API version
For older versions of Android, check out [the branches](https://github.com/seedvault-app/seedvault/branches).
Trying to use an older branch on a newer version may lead to issues and is not something we can support.
## Getting Started
- Check out [the wiki](https://github.com/seedvault-app/seedvault/wiki) for information on building the application with
AOSP.
@ -44,6 +53,7 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need
* `android.permission.MANAGE_DOCUMENTS` to retrieve the available storage roots (optional) for better UX.
* `android.permission.USE_BIOMETRIC` to authenticate saving a new recovery code
* `android.permission.INTERACT_ACROSS_USERS_FULL` to use storage roots in other users (optional).
* `android.permission.POST_NOTIFICATIONS` to inform users about backup status and errors.
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/seedvault-app/seedvault.

View file

@ -18,7 +18,7 @@ android {
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion 29 // leave at 29 for robolectric tests
minSdkVersion 32 // leave at 32 for robolectric tests
targetSdkVersion rootProject.ext.targetSdkVersion
versionNameSuffix "-$gitDescribe"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -38,12 +38,12 @@ android {
abortOnError true
}
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
languageVersion = "1.4"
jvmTarget = JavaVersion.VERSION_11.toString()
languageVersion = "1.6"
}
testOptions {
unitTests.all {
@ -122,13 +122,13 @@ dependencies {
* You can copy these libraries from ~/.gradle/caches/modules-2/files-2.1
*/
// later versions than 2.1.1 require newer kotlin version
// implementation "io.insert-koin:koin-core-jvm:3.1.2"
// implementation "io.insert-koin:koin-android:3.1.2"
// implementation "io.insert-koin:koin-core-jvm:3.2.0"
// implementation "io.insert-koin:koin-android:3.2.0"
implementation fileTree(include: ['*.jar'], dir: "${rootProject.rootDir}/libs/koin-android")
implementation fileTree(include: ['*.aar'], dir: "${rootProject.rootDir}/libs/koin-android")
// implementation "cash.z.ecc.android:kotlin-bip39:1.0.2"
implementation fileTree(include: ['kotlin-bip39-1.0.2.jar'], dir: "${rootProject.rootDir}/libs")
// implementation "cash.z.ecc.android:kotlin-bip39:1.0.4"
implementation fileTree(include: ['kotlin-bip39-jvm-1.0.4.jar'], dir: "${rootProject.rootDir}/libs")
/**
* Test Dependencies (do not concern the AOSP build)
@ -138,10 +138,11 @@ dependencies {
// anything less than 'implementation' fails tests run with gradlew
testImplementation rootProject.ext.aosp_libs
testImplementation 'androidx.test.ext:junit:1.1.3'
testImplementation('org.robolectric:robolectric:4.3.1') { // 4.4 has issue with non-idle Looper
testImplementation('org.robolectric:robolectric:4.8.1') {
// https://github.com/robolectric/robolectric/issues/5245
exclude group: "com.google.auto.service", module: "auto-service"
}
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation "org.junit.jupiter:junit-jupiter-api:$junit5_version"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5_version"
testImplementation "io.mockk:mockk:$mockk_version"
@ -159,9 +160,6 @@ apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
gradle.projectsEvaluated {
tasks.withType(JavaCompile) {
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
options.compilerArgs.addAll(['--release', '8'])
}
options.compilerArgs.add('-Xbootclasspath/p:app/libs/android.jar:app/libs/libcore.jar')
}
}

Binary file not shown.

Binary file not shown.

View file

@ -7,13 +7,11 @@
<activity
android:name="com.stevesoltys.seedvault.settings.SettingsActivity"
android:exported="true"
android:permission=""
tools:replace="android:permission" />
tools:remove="android:permission" />
<activity
android:name="com.stevesoltys.seedvault.restore.RestoreActivity"
android:exported="true"
android:permission=""
tools:replace="android:permission" />
tools:remove="android:permission" />
</application>
</manifest>

View file

@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.stevesoltys.seedvault"
android:versionCode="31000301"
android:versionName="12-3.0">
android:versionCode="33000301"
android:versionName="13-3.1">
<!--
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
The version name is the targeted Android version followed by - and our own version name.
@ -16,7 +16,11 @@
<!-- This is needed to check for internet access when backup is stored on network storage -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- This is needed to retrieve the available storage roots -->
<!-- This is needed to inform users about backup status and errors -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- (Optional) This is needed to retrieve the available storage roots.
The user needs to manually select a storage root, if not granted. -->
<uses-permission
android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" />
@ -50,6 +54,16 @@
<!-- Used to authenticate saving a new recovery code -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- This is needed to query content providers in other users -->
<uses-permission
android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
tools:ignore="ProtectedPermissions" />
<!-- Used to get logcat for system part of backup API, gets permission dialog -->
<uses-permission
android:name="android.permission.READ_LOGS"
tools:ignore="ProtectedPermissions" />
<!-- Permission used to open settings -->
<permission
android:name="com.stevesoltys.seedvault.OPEN_SETTINGS"
@ -60,11 +74,6 @@
android:name="com.stevesoltys.seedvault.RESTORE_BACKUP"
android:protectionLevel="system|signature" />
<!-- This is needed to query content providers in other users -->
<uses-permission
android:name="android.permission.INTERACT_ACROSS_USERS_FULL"
tools:ignore="ProtectedPermissions" />
<application
android:name=".App"
android:allowBackup="false"

View file

@ -1,6 +1,5 @@
package com.stevesoltys.seedvault.restore.install
import android.os.Build.VERSION.SDK_INT
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
@ -81,10 +80,8 @@ internal class InstallProgressAdapter(
IN_PROGRESS -> {
appStatus.visibility = INVISIBLE
progressBar.visibility = VISIBLE
if (SDK_INT >= 30) {
progressBar.stateDescription =
context.getString(R.string.restore_app_status_installing)
}
progressBar.stateDescription =
context.getString(R.string.restore_app_status_installing)
}
SUCCEEDED -> {
appStatus.setImageResource(R.drawable.ic_check_green)

View file

@ -106,7 +106,7 @@ internal class AppListRetriever(
time = time,
status = status
)
}.sortedBy { it.name.toLowerCase(locale) }
}.sortedBy { it.name.lowercase(locale) }
}
private fun getNotAllowedApps(): List<AppStatus> {
@ -120,7 +120,7 @@ internal class AppListRetriever(
time = 0,
status = FAILED_NOT_ALLOWED
)
}.sortedBy { it.name.toLowerCase(locale) }
}.sortedBy { it.name.lowercase(locale) }
}
private fun getIcon(packageName: String): Drawable = when (packageName) {

View file

@ -1,15 +1,35 @@
package com.stevesoltys.seedvault.settings
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads
import com.stevesoltys.seedvault.transport.backup.PackageService
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class ExpertSettingsFragment : PreferenceFragmentCompat() {
private val viewModel: SettingsViewModel by sharedViewModel()
private val packageService: PackageService by inject()
// TODO set mimeType when upgrading androidx lib
private val createFileLauncher = registerForActivityResult(CreateDocument()) { uri ->
viewModel.onLogcatUriReceived(uri)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads {
setPreferencesFromResource(R.xml.settings_expert, rootKey)
}
findPreference<Preference>("logcat")?.setOnPreferenceClickListener {
val versionName = packageService.getVersionName(requireContext().packageName) ?: "ver"
val timestamp = System.currentTimeMillis()
val name = "seedvault-$versionName-$timestamp.txt"
createFileLauncher.launch(name)
true
}
}
override fun onStart() {

View file

@ -105,7 +105,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
dialog.dismiss()
}
.setNegativeButton(R.string.settings_backup_apk_dialog_disable) { dialog, _ ->
apkBackup.isChecked = enable
apkBackup.isChecked = false
dialog.dismiss()
}
.show()
@ -130,14 +130,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.lastBackupTime.observe(viewLifecycleOwner, { time ->
viewModel.lastBackupTime.observe(viewLifecycleOwner) { time ->
setAppBackupStatusSummary(time)
})
}
val backupFiles: Preference = findPreference("backup_files")!!
viewModel.filesSummary.observe(viewLifecycleOwner, { summary ->
viewModel.filesSummary.observe(viewLifecycleOwner) { summary ->
backupFiles.summary = summary
})
}
}
override fun onStart() {
@ -160,10 +160,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
if (resources.getBoolean(R.bool.show_restore_in_settings)) {
menuRestore?.isVisible = true
}
viewModel.backupPossible.observe(viewLifecycleOwner, { possible ->
viewModel.backupPossible.observe(viewLifecycleOwner) { possible ->
menuBackupNow?.isEnabled = possible
menuRestore?.isEnabled = possible
})
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {

View file

@ -11,6 +11,7 @@ import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.Uri
import android.os.Process.myUid
import android.provider.Settings
import android.util.Log
import android.widget.Toast
@ -35,8 +36,11 @@ import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService
import java.io.IOException
import java.lang.Runtime.getRuntime
import java.util.concurrent.TimeUnit.HOURS
private const val TAG = "SettingsViewModel"
@ -193,9 +197,9 @@ internal class SettingsViewModel(
@UiThread
fun loadFilesSummary() = viewModelScope.launch {
val uriSummary = storageBackup.getUriSummaryString()
_filesSummary.value = if (uriSummary.isEmpty()) {
_filesSummary.value = uriSummary.ifEmpty {
app.getString(R.string.settings_backup_files_summary)
} else uriSummary
}
}
/**
@ -233,4 +237,28 @@ internal class SettingsViewModel(
BackupJobService.cancelJob(app)
}
fun onLogcatUriReceived(uri: Uri?) = viewModelScope.launch(Dispatchers.IO) {
if (uri == null) {
onLogcatError()
return@launch
}
// 1000 is system uid, needed to get backup logs from the OS code.
val command = "logcat -d --uid=1000,${myUid()} *:V"
try {
app.contentResolver.openOutputStream(uri, "wt")?.use { outputStream ->
getRuntime().exec(command).inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
}
} ?: throw IOException("OutputStream was null")
} catch (e: Exception) {
Log.e(TAG, "Error saving logcat ", e)
onLogcatError()
}
}
private suspend fun onLogcatError() = withContext(Dispatchers.Main) {
val str = app.getString(R.string.settings_expert_logcat_error)
Toast.makeText(app, str, LENGTH_LONG).show()
}
}

View file

@ -154,7 +154,8 @@ internal class ApkBackup(
streamGetter: suspend (name: String) -> OutputStream,
): List<ApkSplit> {
check(packageInfo.splitNames != null)
val splitSourceDirs = packageInfo.applicationInfo.splitSourceDirs
// attention: though not documented, splitSourceDirs can be null
val splitSourceDirs = packageInfo.applicationInfo.splitSourceDirs ?: emptyArray()
check(packageInfo.splitNames.size == splitSourceDirs.size) {
"Size Mismatch! ${packageInfo.splitNames.size} != ${splitSourceDirs.size} " +
"splitNames is ${packageInfo.splitNames.toList()}, " +

View file

@ -381,7 +381,13 @@ internal class BackupCoordinator(
}
// hook in here to back up APKs of apps that are otherwise not allowed for backup
if (isPmBackup && settingsManager.canDoBackupNow()) {
backUpApksOfNotBackedUpPackages()
try {
backUpApksOfNotBackedUpPackages()
} catch (e: Exception) {
Log.e(TAG, "Error backing up APKs of opt-out apps: ", e)
// We are re-throwing this, because we want to know about problems here
throw e
}
}
}
result

View file

@ -209,6 +209,7 @@ internal class KVBackup(
else state.db.close()
TRANSPORT_OK
} catch (e: IOException) {
Log.e(TAG, "Error uploading DB", e)
TRANSPORT_ERROR
} finally {
this.state = null

View file

@ -2,7 +2,6 @@ package com.stevesoltys.seedvault.ui
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build.VERSION.SDK_INT
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
@ -40,12 +39,10 @@ internal abstract class AppViewHolder(protected val v: View) : RecyclerView.View
appInfo.visibility = GONE
appStatus.visibility = INVISIBLE
progressBar.visibility = VISIBLE
if (SDK_INT >= 30) {
progressBar.stateDescription = context.getString(
if (isRestore) R.string.restore_restoring
else R.string.backup_app_in_progress
)
}
progressBar.stateDescription = context.getString(
if (isRestore) R.string.restore_restoring
else R.string.backup_app_in_progress
)
} else {
appStatus.visibility = VISIBLE
progressBar.visibility = INVISIBLE

View file

@ -43,7 +43,7 @@ internal class NotificationBackupObserver(
* @param currentBackupPackage The name of the package that now being backed up.
* @param backupProgress Current progress of backup for the package.
*/
override fun onUpdate(currentBackupPackage: String, backupProgress: BackupProgress) {
override fun onUpdate(currentBackupPackage: String?, backupProgress: BackupProgress) {
showProgressNotification(currentBackupPackage)
}
@ -57,7 +57,7 @@ internal class NotificationBackupObserver(
* that was initialized
* @param status Zero on success; a nonzero error code if the backup operation failed.
*/
override fun onResult(target: String, status: Int) {
override fun onResult(target: String?, status: Int) {
if (isLoggable(TAG, INFO)) {
Log.i(TAG, "Completed. Target: $target, status: $status")
}
@ -81,8 +81,8 @@ internal class NotificationBackupObserver(
nm.onBackupFinished(success, numBackedUp)
}
private fun showProgressNotification(packageName: String) {
if (currentPackage == packageName) return
private fun showProgressNotification(packageName: String?) {
if (packageName == null || currentPackage == packageName) return
if (isLoggable(TAG, INFO)) {
"Showing progress notification for $currentPackage $numPackages/$expectedPackages".let {

View file

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.isDebugBuild
import com.stevesoltys.seedvault.ui.BackupActivity
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import org.koin.androidx.viewmodel.ext.android.viewModel
@ -15,7 +16,7 @@ class RecoveryCodeActivity : BackupActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(FLAG_SECURE)
if (!isDebugBuild()) window.addFlags(FLAG_SECURE)
setContentView(R.layout.activity_recovery_code)

View file

@ -6,7 +6,6 @@ import android.content.Intent
import android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_STRONG
import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import android.hardware.biometrics.BiometricPrompt
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.CancellationSignal
import android.view.LayoutInflater
@ -22,7 +21,6 @@ import android.widget.TextView
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat.getMainExecutor
@ -122,9 +120,9 @@ class RecoveryCodeInputFragment : Fragment() {
newCodeButton.visibility = if (forStoringNewCode) GONE else VISIBLE
newCodeButton.setOnClickListener { generateNewCode() }
viewModel.existingCodeChecked.observeEvent(viewLifecycleOwner, { verified ->
viewModel.existingCodeChecked.observeEvent(viewLifecycleOwner) { verified ->
onExistingCodeChecked(verified)
})
}
if (forStoringNewCode && isDebugBuild() && !viewModel.isRestore) debugPreFill()
}
@ -147,7 +145,7 @@ class RecoveryCodeInputFragment : Fragment() {
}
if (forStoringNewCode) {
val keyguardManager = requireContext().getSystemService(KeyguardManager::class.java)
if (SDK_INT >= 30 && keyguardManager.isDeviceSecure) {
if (keyguardManager.isDeviceSecure) {
// if we have a lock-screen secret, we can ask for it before storing the code
storeNewCodeAfterAuth(input)
} else {
@ -159,7 +157,6 @@ class RecoveryCodeInputFragment : Fragment() {
}
}
@RequiresApi(30)
private fun storeNewCodeAfterAuth(input: List<CharSequence>) {
val context = requireContext()
val biometricPrompt = BiometricPrompt.Builder(context)

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?android:attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>

View file

@ -47,9 +47,11 @@
<string name="settings_expert_quota_title">Unlimited app quota</string>
<string name="settings_expert_quota_summary">Do not impose a limitation on the size of app backups.\n\nWarning: This can fill up your storage location quickly. Not needed for most apps.</string>
<string name="settings_expert_logcat_title">Save app log</string>
<string name="settings_expert_logcat_summary"></string>
<string name="settings_expert_logcat_error">Could not save app log</string>
<string name="settings_expert_logcat_summary">Sending this to the developers can help diagnose bugs.\n\nAlways review it to ensure it has no personally identifiable info, and delete it afterwards!</string>
<string name="settings_expert_logcat_error">Could not save app log</string>
<!-- Storage Location -->
<string name="storage_fragment_backup_title">Choose where to store backups</string>
<string name="storage_fragment_restore_title">Where to find your backups?</string>

View file

@ -5,4 +5,9 @@
android:key="unlimited_quota"
android:summary="@string/settings_expert_quota_summary"
android:title="@string/settings_expert_quota_title" />
</PreferenceScreen>
<Preference
android:icon="@drawable/ic_bug_report"
android:key="logcat"
android:summary="@string/settings_expert_logcat_summary"
android:title="@string/settings_expert_logcat_title" />
</PreferenceScreen>

View file

@ -41,7 +41,7 @@ import kotlin.random.Random
@Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class)
@Config(
sdk = [29], // robolectric does not support 30, yet
sdk = [32], // robolectric does not support 33, yet
application = TestApp::class
)
class MetadataManagerTest {

View file

@ -19,7 +19,7 @@ import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
@Config(
sdk = [29], // robolectric does not support 30, yet
sdk = [32], // robolectric does not support 33, yet
application = TestApp::class
)
internal class DocumentFileTest {

View file

@ -133,14 +133,8 @@ internal class ApkBackupRestoreTest : TransportTest() {
every { strictContext.cacheDir } returns tmpFile
every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns inputStream
every { pm.getPackageArchiveInfo(capture(apkPath), any()) } returns packageInfo
every {
@Suppress("UNRESOLVED_REFERENCE")
pm.loadItemIcon(
packageInfo.applicationInfo,
packageInfo.applicationInfo
)
} returns icon
every { pm.getPackageArchiveInfo(capture(apkPath), any<Int>()) } returns packageInfo
every { applicationInfo.loadIcon(pm) } returns icon
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
every {
splitCompatChecker.isCompatible(metadata.deviceName, listOf(splitName))

View file

@ -1,7 +1,6 @@
package com.stevesoltys.seedvault.restore.install
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_INSTALLED
import android.content.pm.ApplicationInfo.FLAG_SYSTEM
import android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
@ -112,7 +111,7 @@ internal class ApkRestoreTest : TransportTest() {
every { strictContext.cacheDir } returns File(tmpDir.toString())
every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
every { pm.getPackageArchiveInfo(any(), any<Int>()) } returns packageInfo
every { storagePlugin.providerPackageName } returns storageProviderPackageName
apkRestore.restore(backup).collectIndexed { i, value ->
@ -176,14 +175,8 @@ internal class ApkRestoreTest : TransportTest() {
coEvery {
legacyStoragePlugin.getApkInputStream(token, packageName, "")
} returns apkInputStream
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
every {
@Suppress("UNRESOLVED_REFERENCE")
pm.loadItemIcon(
packageInfo.applicationInfo,
packageInfo.applicationInfo
)
} returns icon
every { pm.getPackageArchiveInfo(any(), any<Int>()) } returns packageInfo
every { applicationInfo.loadIcon(pm) } returns icon
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
coEvery {
apkInstaller.install(match { it.size == 1 }, packageName, installerName, any())
@ -200,13 +193,11 @@ internal class ApkRestoreTest : TransportTest() {
runBlocking {
val packageMetadata = this@ApkRestoreTest.packageMetadata.copy(system = true)
packageMetadataMap[packageName] = packageMetadata
packageInfo.applicationInfo = mockk()
val installedPackageInfo: PackageInfo = mockk()
val willFail = Random.nextBoolean()
val isSystemApp = Random.nextBoolean()
cacheBaseApkAndGetInfo(tmpDir)
every { packageInfo.applicationInfo.loadIcon(pm) } returns icon
every { storagePlugin.providerPackageName } returns storageProviderPackageName
if (willFail) {
@ -214,7 +205,7 @@ internal class ApkRestoreTest : TransportTest() {
pm.getPackageInfo(packageName, 0)
} throws PackageManager.NameNotFoundException()
} else {
installedPackageInfo.applicationInfo = ApplicationInfo().apply {
installedPackageInfo.applicationInfo = mockk {
flags =
if (!isSystemApp) FLAG_INSTALLED else FLAG_SYSTEM or FLAG_UPDATED_SYSTEM_APP
}
@ -421,14 +412,8 @@ internal class ApkRestoreTest : TransportTest() {
every { strictContext.cacheDir } returns File(tmpDir.toString())
every { crypto.getNameForApk(salt, packageName, "") } returns name
coEvery { storagePlugin.getInputStream(token, name) } returns apkInputStream
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
every {
@Suppress("UNRESOLVED_REFERENCE")
pm.loadItemIcon(
packageInfo.applicationInfo,
packageInfo.applicationInfo
)
} returns icon
every { pm.getPackageArchiveInfo(any(), any<Int>()) } returns packageInfo
every { applicationInfo.loadIcon(pm) } returns icon
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
}

View file

@ -22,7 +22,7 @@ import kotlin.random.Random
@RunWith(AndroidJUnit4::class)
@Config(
sdk = [29], // robolectric does not support 30, yet
sdk = [32], // robolectric does not support 33, yet
application = TestApp::class
)
internal class DeviceInfoTest {
@ -62,12 +62,10 @@ internal class DeviceInfoTest {
assertFalse(deviceInfo.isSupportedLanguage("de"))
// test areUnknownSplitsAllowed
val deviceName = "unknown robolectric"
assertTrue(deviceInfo.areUnknownSplitsAllowed("robolectric robolectric"))
if (onlyOnSameDevice) {
assertTrue(deviceInfo.areUnknownSplitsAllowed(deviceName))
assertFalse(deviceInfo.areUnknownSplitsAllowed("foo bar"))
} else {
assertTrue(deviceInfo.areUnknownSplitsAllowed(deviceName))
assertTrue(deviceInfo.areUnknownSplitsAllowed("foo bar"))
}
}

View file

@ -37,12 +37,13 @@ internal abstract class TransportTest {
protected val sigInfo: SigningInfo = mockk()
protected val token = Random.nextLong()
protected val applicationInfo = mockk<ApplicationInfo> {
flags = FLAG_ALLOW_BACKUP or FLAG_INSTALLED
}
protected val packageInfo = PackageInfo().apply {
packageName = "org.example"
longVersionCode = Random.nextLong()
applicationInfo = ApplicationInfo().apply {
flags = FLAG_ALLOW_BACKUP or FLAG_INSTALLED
}
applicationInfo = this@TransportTest.applicationInfo
signingInfo = sigInfo
}
protected val pmPackageInfo = PackageInfo().apply {

View file

@ -5,7 +5,6 @@ import android.app.backup.BackupTransport.TRANSPORT_NOT_INITIALIZED
import android.app.backup.BackupTransport.TRANSPORT_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.content.pm.PackageInfo
import android.net.Uri
@ -399,7 +398,9 @@ internal class BackupCoordinatorTest : BackupTest() {
PackageInfo().apply {
packageName = "org.example.2"
// the second package does not get backed up, because it is stopped
applicationInfo = ApplicationInfo().apply { flags = FLAG_STOPPED }
applicationInfo = mockk {
flags = FLAG_STOPPED
}
}
)
val packageMetadata: PackageMetadata = mockk()

View file

@ -1,11 +1,11 @@
buildscript {
// 1.3.21 Android 10
// 1.3.61 Android 11
// 1.4.30 Android 12
// 1.6.10 Android 13
// Check:
// https://android.googlesource.com/platform/external/kotlinc/+/refs/tags/android-12.0.0_r2/build.txt
ext.aosp_kotlin_version = '1.4.31' // 1.4.30 breaks Kotlin plugin in Android Studio
ext.kotlin_version = '1.4.31'
// https://android.googlesource.com/platform/external/kotlinc/+/refs/tags/android-13.0.0_r3/build.txt
ext.aosp_kotlin_version = '1.6.10' // 1.6.10-release-923 in AOSP
ext.kotlin_version = '1.6.10'
repositories {
mavenCentral()
@ -15,15 +15,15 @@ buildscript {
//noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.17"
classpath 'com.android.tools.build:gradle:7.0.2'
classpath 'com.android.tools.build:gradle:7.2.2'
}
}
ext {
buildToolsVersion = '31.0.0'
compileSdkVersion = 31
minSdkVersion = 29
targetSdkVersion = 31
buildToolsVersion = '33.0.0'
compileSdkVersion = 33
minSdkVersion = 32
targetSdkVersion = 33
}
apply from: 'gradle/dependencies.gradle'

View file

@ -15,12 +15,12 @@ android {
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = JavaVersion.VERSION_11.toString()
}
testOptions {

View file

@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.calyxos.backup.contacts"
android:versionCode="31000301"
android:versionName="12-3.0">
android:versionCode="33000301"
android:versionName="13-3.1">
<!--
The version code is the targeted SDK_VERSION plus 6 digits for our own version code.
The version name is the targeted Android version followed by - and our own version name.

View file

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<exceptions>
<exception package="com.stevesoltys.seedvault">
<!-- Notifications -->
<permission name="android.permission.POST_NOTIFICATIONS" fixed="false"/>
</exception>
</exceptions>

View file

@ -1,11 +1,13 @@
ext {
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#2943
ext.room_version = "2.3.0-beta02"
// https://android.googlesource.com/platform/external/protobuf/+/refs/tags/android-12.0.0_r2/java/pom.xml#7
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#3901
ext.room_version = "2.4.0-alpha05"
// https://android.googlesource.com/platform/external/protobuf/+/refs/tags/android-13.0.0_r3/java/pom.xml#7
ext.protobuf_version = "3.9.1"
// test dependencies below - these do not care about AOSP and can be freely updated
junit4_version = "4.13.2"
junit5_version = "5.5.2" // careful, upgrading this can change a Cipher's IV size in tests!?
mockk_version = "1.12.0"
junit5_version = "5.7.2" // careful, upgrading this can change a Cipher's IV size in tests!?
mockk_version = "1.12.3"
espresso_version = "3.4.0"
}
@ -37,63 +39,63 @@ ext.kotlin_libs = [
],
coroutines: [
dependencies.create('org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm') {
// https://android.googlesource.com/platform/prebuilts/tools/+/refs/tags/android-12.0.0_r2/common/m2/Android.bp#273
version { strictly '1.4.2' }
// https://android.googlesource.com/platform/external/kotlinx.coroutines/+/refs/tags/android-13.0.0_r3/CHANGES.md
version { strictly '1.5.2' }
},
dependencies.create('org.jetbrains.kotlinx:kotlinx-coroutines-android') {
// https://android.googlesource.com/platform/prebuilts/tools/+/refs/tags/android-12.0.0_r2/common/m2/Android.bp#288
version { strictly '1.3.0' }
// https://android.googlesource.com/platform/external/kotlinx.coroutines/+/refs/tags/android-13.0.0_r3/CHANGES.md
version { strictly '1.5.2' }
},
],
]
ext.std_libs = [
androidx_core: [
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#867
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#1761
dependencies.create('androidx.core:core') {
version { strictly '1.6.0' } // should be 1.6.0-beta03, but that is not even released, yet
version { strictly '1.9.0-alpha03' }
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#833
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#1727
dependencies.create('androidx.core:core-ktx') {
version { strictly '1.5.0-beta02' }
version { strictly '1.9.0-alpha03' }
},
],
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#1189
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#2159
androidx_fragment: dependencies.create('androidx.fragment:fragment-ktx') {
version { strictly '1.4.0-alpha01' }
version { strictly '1.4.0-alpha09' }
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#20
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#57
androidx_activity: dependencies.create('androidx.activity:activity-ktx') {
version { strictly '1.3.0-alpha03' }
version { strictly '1.4.0-alpha02' }
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#2695
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#3597
androidx_preference: dependencies.create('androidx.preference:preference') {
version { strictly '1.1.1' } // should be 1.2.0-alpha01, but that is not even released, yet
version { strictly '1.2.0-alpha01' }
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#1820
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#2754
androidx_lifecycle_viewmodel_ktx: dependencies.create('androidx.lifecycle:lifecycle-viewmodel-ktx') {
version { strictly '2.4.0-alpha01' }
version { strictly '2.4.0-alpha03' } // 2.4.0-alpha04 in AOSP but was never released
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#1618
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#2550
androidx_lifecycle_livedata_ktx: dependencies.create('androidx.lifecycle:lifecycle-livedata-ktx') {
version { strictly '2.4.0-alpha01' }
version { strictly '2.4.0-alpha03' } // 2.4.0-alpha04 in AOSP but was never released
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/extras/constraint-layout-x/Android.bp#39
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/extras/constraint-layout-x/Android.bp#39
androidx_constraintlayout: dependencies.create('androidx.constraintlayout:constraintlayout') {
version { strictly '2.0.0-beta7' }
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#969
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#1865
androidx_documentfile: dependencies.create('androidx.documentfile:documentfile') {
version { strictly '1.1.0-alpha01' }
version { strictly '1.1.0-alpha01' } // 1.1.0-alpha02 in AOSP but not released yet
},
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/extras/material-design-x/Android.bp#6
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/extras/material-design-x/Android.bp#6
com_google_android_material: dependencies.create('com.google.android.material:material') {
version { strictly '1.4.0' }
version { strictly '1.6.0-alpha03' } // 1.6.0-alpha0301 in AOSP
},
]
ext.lint_libs = [
exceptions: 'com.github.thirdegg:lint-rules:0.0.6-beta'
exceptions: 'com.github.thirdegg:lint-rules:0.1.0'
]
ext.storage_libs = [
@ -103,7 +105,9 @@ ext.storage_libs = [
com_google_protobuf_javalite: dependencies.create('com.google.protobuf:protobuf-javalite') {
version { strictly "$protobuf_version" }
},
// https://github.com/google/tink/releases
com_google_crypto_tink_android: dependencies.create('com.google.crypto.tink:tink-android') {
version { strictly '1.6.1' }
// careful with upgrading tink, so old backups continue to be decryptable
version { strictly '1.7.0' }
},
]

View file

@ -1,7 +1,7 @@
#Tue Aug 04 14:40:48 BRT 2020
#Fri Aug 19 10:56:09 IST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b
zipStoreBase=GRADLE_USER_HOME
distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302

View file

@ -1,5 +1,5 @@
java_import {
name: "seedvault-lib-kotlin-bip39",
jars: ["kotlin-bip39-1.0.2.jar"],
jars: ["kotlin-bip39-jvm-1.0.4.jar"],
sdk_version: "current",
}

View file

@ -1,11 +1,11 @@
android_library_import {
name: "seedvault-lib-koin-android",
aars: ["koin-android-3.1.2.aar"],
aars: ["koin-android-3.2.0.aar"],
sdk_version: "current",
}
java_import {
name: "seedvault-lib-koin-core-jvm",
jars: ["koin-core-jvm-3.1.2.jar"],
jars: ["koin-core-jvm-3.2.0.jar"],
sdk_version: "current",
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -27,11 +27,11 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
}
lintOptions {

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.grobox.storagebackuptester">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".App"
android:allowBackup="false"

View file

@ -1,11 +1,7 @@
package de.grobox.storagebackuptester
import android.Manifest.permission.ACCESS_MEDIA_LOCATION
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
@ -21,7 +17,6 @@ import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import android.widget.Toast.LENGTH_SHORT
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.RecyclerView
@ -43,13 +38,6 @@ open class LogFragment : Fragment() {
private lateinit var button: Button
private val adapter = LogAdapter()
private val permissionRequest =
registerForActivityResult(RequestMultiplePermissions()) { grantedMap ->
if (grantedMap[WRITE_EXTERNAL_STORAGE] == true) {
Toast.makeText(requireContext(), "Please try again now!", LENGTH_SHORT).show()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -62,19 +50,19 @@ open class LogFragment : Fragment() {
progressBar = v.findViewById(R.id.progressBar)
horizontalProgressBar = v.findViewById(R.id.horizontalProgressBar)
button = v.findViewById(R.id.button)
viewModel.backupLog.observe(viewLifecycleOwner, { progress ->
viewModel.backupLog.observe(viewLifecycleOwner) { progress ->
progress.text?.let { adapter.addItem(it) }
horizontalProgressBar.max = progress.total
horizontalProgressBar.setProgress(progress.current, true)
list.postDelayed({
list.scrollToPosition(adapter.itemCount - 1)
}, 50)
})
viewModel.backupButtonEnabled.observe(viewLifecycleOwner, { enabled ->
}
viewModel.backupButtonEnabled.observe(viewLifecycleOwner) { enabled ->
button.isEnabled = enabled
progressBar.visibility = if (enabled) INVISIBLE else VISIBLE
if (!enabled) adapter.clear()
})
}
button.setOnClickListener {
if (!checkPermission()) return@setOnClickListener
viewModel.simulateBackup()
@ -120,23 +108,13 @@ open class LogFragment : Fragment() {
}
private fun checkPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= 30) {
if (Environment.isExternalStorageManager()) return true
Toast.makeText(requireContext(), "Permission needed", LENGTH_SHORT).show()
val i = Intent(ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${requireContext().packageName}")
}
startActivity(i)
false
} else {
if (requireContext().checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
true
} else {
Toast.makeText(requireContext(), "No storage permission", LENGTH_SHORT).show()
permissionRequest.launch(arrayOf(WRITE_EXTERNAL_STORAGE, ACCESS_MEDIA_LOCATION))
false
}
if (Environment.isExternalStorageManager()) return true
Toast.makeText(requireContext(), "Permission needed", LENGTH_SHORT).show()
val i = Intent(ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
data = Uri.parse("package:${requireContext().packageName}")
}
startActivity(i)
return false
}
}

View file

@ -1,6 +1,5 @@
package de.grobox.storagebackuptester.settings
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.provider.DocumentsContract
import android.provider.MediaStore
@ -39,16 +38,14 @@ class InfoFragment : MediaScanFragment() {
} catch (e: IllegalArgumentException) {
e.toString()
}
val gen = if (SDK_INT >= 30) try {
val gen = try {
MediaStore.getGeneration(context, volumeName)
} catch (e: IllegalArgumentException) {
e.toString()
} else null
}
sb.appendLine(" $volumeName")
sb.appendLine(" version: $version")
if (gen != null) {
sb.appendLine(" generation: $gen")
}
sb.appendLine(" generation: $gen")
}
sb.appendLine()
sb.appendLine("Media files smaller than 100 KB: ${mediaFilesSmallerThan(100 * 1024)}")

View file

@ -17,11 +17,15 @@ android_library {
"seedvault-lib-tink-android",
"libprotobuf-java-lite",
"androidx.core_core-ktx",
"androidx.fragment_fragment-ktx",
"androidx.activity_activity-ktx",
"androidx.documentfile_documentfile",
"androidx.lifecycle_lifecycle-viewmodel-ktx",
"androidx.room_room-runtime",
"androidx-constraintlayout_constraintlayout",
"com.google.android.material_material",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
],
plugins: [
"androidx.room_room-compiler-plugin",
@ -37,6 +41,6 @@ android_library {
java_import {
name: "seedvault-lib-tink-android",
jars: ["libs/tink-android-1.6.1.jar"],
jars: ["libs/tink-android-1.7.0.jar"],
sdk_version: "current",
}

View file

@ -4,7 +4,7 @@ plugins {
id 'kotlin-android'
id 'kotlin-kapt'
id "org.jlleitschuh.gradle.ktlint" version "10.2.0"
id 'org.jetbrains.dokka' version '1.4.30'
id 'org.jetbrains.dokka' version "$kotlin_version"
}
android {
@ -28,12 +28,12 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
languageVersion = "1.4"
jvmTarget = JavaVersion.VERSION_11.toString()
languageVersion = "1.6"
freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
freeCompilerArgs += '-Xexplicit-api=strict'
}

Binary file not shown.

View file

@ -3,13 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.calyxos.backup.storage">
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />

View file

@ -111,11 +111,12 @@ internal class Backup(
}
Log.e(TAG, "Changed files backup took $duration")
} finally {
backupObserver?.onBackupComplete(duration?.toLongMilliseconds())
backupObserver?.onBackupComplete(duration?.inWholeMilliseconds)
}
}
@Throws(IOException::class, GeneralSecurityException::class)
@OptIn(ExperimentalTime::class)
private suspend fun backupFiles(
filesResult: FileScannerResult,
availableChunkIds: HashSet<String>,

View file

@ -8,7 +8,7 @@ import org.calyxos.backup.storage.measure
import org.calyxos.backup.storage.plugin.SnapshotRetriever
import java.io.IOException
import java.security.GeneralSecurityException
import java.util.concurrent.TimeUnit.MILLISECONDS
import kotlin.time.DurationUnit.MILLISECONDS
import kotlin.time.ExperimentalTime
import kotlin.time.toDuration

View file

@ -18,6 +18,7 @@ import org.calyxos.backup.storage.plugin.saf.DocumentFileExt.listFilesBlocking
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.ExperimentalTime
private val folderRegex = Regex("^[a-f0-9]{16}\\.sv$")
private val chunkFolderRegex = Regex("[a-f0-9]{2}")
@ -89,6 +90,7 @@ public abstract class SafStoragePlugin(
* Chunk folders will get cached in the given [chunkFolders] for faster access.
*/
@Throws(IOException::class)
@OptIn(ExperimentalTime::class)
private suspend fun populateChunkFolders(
folder: DocumentFile,
chunkFolders: HashMap<String, DocumentFile>,
@ -126,6 +128,7 @@ public abstract class SafStoragePlugin(
}
@Throws(IOException::class)
@OptIn(ExperimentalTime::class)
private fun createMissingChunkFolders(
root: DocumentFile,
chunkFolders: HashMap<String, DocumentFile>,

View file

@ -47,7 +47,7 @@ internal class Pruner(
}
}
Log.i(TAG, "Pruning took $duration")
backupObserver?.onPruneComplete(duration.toLongMilliseconds())
backupObserver?.onPruneComplete(duration.inWholeMilliseconds)
}
@Throws(IOException::class, GeneralSecurityException::class)

View file

@ -4,7 +4,6 @@ import android.content.ContentValues
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Environment.getExternalStorageDirectory
import android.provider.MediaStore.MediaColumns
import android.util.Log
@ -39,13 +38,7 @@ internal class FileRestore(
val finalTag: String
when {
file.mediaFile != null -> {
bytes = if (SDK_INT < 30) {
// MediaProvider on API 29 doesn't let us write files into any folders freely,
// so don't attempt to restore via MediaStore API
restoreFile(file, streamWriter)
} else {
restoreFile(file.mediaFile, streamWriter)
}
bytes = restoreFile(file.mediaFile, streamWriter)
finalTag = "M$tag"
}
file.docFile != null -> {
@ -112,10 +105,7 @@ internal class FileRestore(
// changing owner requires backup permission
put(MediaColumns.OWNER_PACKAGE_NAME, mediaFile.ownerPackageName)
put(MediaColumns.IS_PENDING, 1)
if (SDK_INT >= 30) {
val isFavorite = if (mediaFile.isFavorite) 1 else 0
put(MediaColumns.IS_FAVORITE, isFavorite)
}
put(MediaColumns.IS_FAVORITE, if (mediaFile.isFavorite) 1 else 0)
}
val contentUri = MediaType.fromBackupMediaType(mediaFile.type).contentUri
val uri = contentResolver.insert(contentUri, contentValues)!!

View file

@ -52,6 +52,7 @@ internal class Restore(
MultiChunkRestore(context, storagePlugin, fileRestore, streamCrypto, streamKey)
}
@OptIn(ExperimentalTime::class)
fun getBackupSnapshots(): Flow<SnapshotResult> = flow {
val numSnapshots: Int
val time = measure {
@ -138,7 +139,7 @@ internal class Restore(
Log.e(TAG, "Restoring ${split.multiChunkFiles.size} multi chunks took $multiChunkDuration.")
val totalDuration = smallFilesDuration + singleChunkDuration + multiChunkDuration
observer?.onRestoreComplete(totalDuration.toLongMilliseconds())
observer?.onRestoreComplete(totalDuration.inWholeMilliseconds)
Log.e(TAG, "Restored $restoredFiles/$filesTotal files.")
}

View file

@ -11,6 +11,7 @@ import org.calyxos.backup.storage.content.DocFile
import org.calyxos.backup.storage.content.MediaFile
import org.calyxos.backup.storage.db.UriStore
import org.calyxos.backup.storage.measure
import kotlin.time.ExperimentalTime
internal class FileScannerResult(
val smallFiles: List<ContentFile>,
@ -30,6 +31,7 @@ internal class FileScanner(
private const val FILES_LARGE = "large"
}
@OptIn(ExperimentalTime::class)
fun getFiles(): FileScannerResult {
// scan both APIs
val mediaFiles = ArrayList<ContentFile>()

View file

@ -5,13 +5,11 @@ import android.content.ContentUris
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.provider.MediaStore.MediaColumns.IS_DOWNLOAD
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
@ -23,7 +21,7 @@ import java.io.File
public class MediaScanner(context: Context) {
private companion object {
private val PROJECTION_29 = arrayOf(
private val PROJECTION = arrayOf(
MediaStore.MediaColumns._ID,
MediaStore.MediaColumns.RELATIVE_PATH,
MediaStore.MediaColumns.DISPLAY_NAME,
@ -31,10 +29,6 @@ public class MediaScanner(context: Context) {
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.OWNER_PACKAGE_NAME,
MediaStore.MediaColumns.VOLUME_NAME,
)
@RequiresApi(30)
private val PROJECTION_30 = arrayOf(
MediaStore.MediaColumns.IS_FAVORITE,
MediaStore.MediaColumns.GENERATION_MODIFIED,
)
@ -59,7 +53,7 @@ public class MediaScanner(context: Context) {
internal fun scanMediaUri(uri: Uri, extraQuery: String? = null): List<MediaFile> {
val extras = Bundle().apply {
val query = StringBuilder()
if (SDK_INT >= 30 && uri != MediaType.Downloads.contentUri) {
if (uri != MediaType.Downloads.contentUri) {
query.append("$IS_DOWNLOAD=0")
}
extraQuery?.let {
@ -68,8 +62,7 @@ public class MediaScanner(context: Context) {
}
if (query.isNotEmpty()) putString(QUERY_ARG_SQL_SELECTION, query.toString())
}
val projection = if (SDK_INT >= 30) PROJECTION_29 + PROJECTION_30 else PROJECTION_29
val cursor = contentResolver.query(uri, projection, extras, null)
val cursor = contentResolver.query(uri, PROJECTION, extras, null)
return ArrayList<MediaFile>(cursor?.count ?: 0).apply {
cursor?.use { c ->
while (c.moveToNext()) add(createMediaFile(c, uri))
@ -94,13 +87,9 @@ public class MediaScanner(context: Context) {
dir = cursor.getString(PROJECTION_PATH),
fileName = cursor.getString(PROJECTION_NAME),
dateModified = cursor.getLongOrNull(PROJECTION_DATE_MODIFIED),
generationModified = if (SDK_INT >= 30) cursor.getLongOrNull(
PROJECTION_GENERATION_MODIFIED
) else null,
generationModified = cursor.getLongOrNull(PROJECTION_GENERATION_MODIFIED),
size = cursor.getLong(PROJECTION_SIZE),
isFavorite = if (SDK_INT >= 30) {
cursor.getIntOrNull(PROJECTION_IS_FAVORITE) == 1
} else false,
isFavorite = cursor.getIntOrNull(PROJECTION_IS_FAVORITE) == 1,
ownerPackageName = cursor.getStringOrNull(PROJECTION_OWNER_PACKAGE_NAME),
volume = cursor.getString(PROJECTION_VOLUME_NAME)
)

View file

@ -7,7 +7,6 @@ import android.app.NotificationManager
import android.app.NotificationManager.IMPORTANCE_LOW
import android.app.PendingIntent
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
@ -145,7 +144,7 @@ internal class Notifications(private val context: Context) {
setShowWhen(false)
setWhen(System.currentTimeMillis())
setProgress(expected, transferred, expected == 0)
if (SDK_INT >= 31) setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE)
setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE)
}.build()
}