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"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Instrumentation tests: app" type="AndroidTestRunConfigurationType" factoryName="Android Instrumented Tests" singleton="true"> <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="TESTING_TYPE" value="0" />
<option name="METHOD_NAME" value="" /> <option name="METHOD_NAME" value="" />
<option name="CLASS_NAME" value="" /> <option name="CLASS_NAME" value="" />
@ -8,10 +8,12 @@
<option name="INSTRUMENTATION_RUNNER_CLASS" value="" /> <option name="INSTRUMENTATION_RUNNER_CLASS" value="" />
<option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" /> <option name="EXTRA_OPTIONS" value="-e notAnnotation androidx.test.filters.LargeTest" />
<option name="INCLUDE_GRADLE_EXTRA_OPTIONS" value="true" /> <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="CLEAR_LOGCAT" value="false" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" /> <option name="SHOW_LOGCAT_AUTOMATICALLY" value="false" />
<option name="SKIP_NOOP_APK_INSTALLATIONS" value="true" /> <option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="FORCE_STOP_RUNNING_APP" value="true" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" /> <option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="DEBUGGER_TYPE" value="Auto" /> <option name="DEBUGGER_TYPE" value="Auto" />
<Auto> <Auto>
@ -40,7 +42,7 @@
<option name="ADVANCED_PROFILING_ENABLED" value="false" /> <option name="ADVANCED_PROFILING_ENABLED" value="false" />
<option name="STARTUP_PROFILING_ENABLED" value="false" /> <option name="STARTUP_PROFILING_ENABLED" value="false" />
<option name="STARTUP_CPU_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="STARTUP_NATIVE_MEMORY_PROFILING_ENABLED" value="false" />
<option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" /> <option name="NATIVE_MEMORY_SAMPLE_RATE_BYTES" value="2048" />
</Profilers> </Profilers>
@ -48,4 +50,4 @@
<option name="Android.Gradle.BeforeRunTask" enabled="true" /> <option name="Android.Gradle.BeforeRunTask" enabled="true" />
</method> </method>
</configuration> </configuration>
</component> </component>

View file

@ -26,12 +26,16 @@ android_app {
static_libs: [ static_libs: [
"kotlin-stdlib-jdk8", "kotlin-stdlib-jdk8",
"androidx.core_core-ktx", "androidx.core_core-ktx",
"androidx.fragment_fragment-ktx",
"androidx.activity_activity-ktx",
"androidx.preference_preference", "androidx.preference_preference",
"androidx.documentfile_documentfile", "androidx.documentfile_documentfile",
"androidx.lifecycle_lifecycle-viewmodel-ktx", "androidx.lifecycle_lifecycle-viewmodel-ktx",
"androidx.lifecycle_lifecycle-livedata-ktx", "androidx.lifecycle_lifecycle-livedata-ktx",
"androidx-constraintlayout_constraintlayout", "androidx-constraintlayout_constraintlayout",
"com.google.android.material_material", "com.google.android.material_material",
"kotlinx-coroutines-android",
"kotlinx-coroutines-core",
// storage backup lib // storage backup lib
"seedvault-lib-storage", "seedvault-lib-storage",
// koin // koin
@ -48,8 +52,9 @@ android_app {
privileged: true, privileged: true,
required: [ required: [
"LocalContactsBackup", "LocalContactsBackup",
"privapp_whitelist_com.stevesoltys.backup", "com.stevesoltys.backup_allowlist",
"com.stevesoltys.backup_whitelist" "com.stevesoltys.backup_default-permissions",
"com.stevesoltys.backup_privapp_allowlist"
], ],
optimize: { optimize: {
enabled: false, enabled: false,
@ -57,17 +62,25 @@ android_app {
} }
prebuilt_etc { 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, system_ext_specific: true,
sub_dir: "permissions", sub_dir: "permissions",
src: "permissions_com.stevesoltys.seedvault.xml", src: "permissions_com.stevesoltys.seedvault.xml",
filename_from_src: true, 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 ## [12-3.0] - 2021-10-13
* Initial release for Android 12 * Initial release for Android 12
* Use the same (faster and more secure) crypto that storage backups use, * 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 ## 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). 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 ## Getting Started
- Check out [the wiki](https://github.com/seedvault-app/seedvault/wiki) for information on building the application with - Check out [the wiki](https://github.com/seedvault-app/seedvault/wiki) for information on building the application with
AOSP. 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.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.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.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 ## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/seedvault-app/seedvault. 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 buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig { defaultConfig {
minSdkVersion 29 // leave at 29 for robolectric tests minSdkVersion 32 // leave at 32 for robolectric tests
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionNameSuffix "-$gitDescribe" versionNameSuffix "-$gitDescribe"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -38,12 +38,12 @@ android {
abortOnError true abortOnError true
} }
compileOptions { compileOptions {
targetCompatibility 1.8 sourceCompatibility = JavaVersion.VERSION_11
sourceCompatibility 1.8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_11.toString()
languageVersion = "1.4" languageVersion = "1.6"
} }
testOptions { testOptions {
unitTests.all { unitTests.all {
@ -122,13 +122,13 @@ dependencies {
* You can copy these libraries from ~/.gradle/caches/modules-2/files-2.1 * You can copy these libraries from ~/.gradle/caches/modules-2/files-2.1
*/ */
// later versions than 2.1.1 require newer kotlin version // 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-core-jvm:3.2.0"
// implementation "io.insert-koin:koin-android:3.1.2" // implementation "io.insert-koin:koin-android:3.2.0"
implementation fileTree(include: ['*.jar'], dir: "${rootProject.rootDir}/libs/koin-android") implementation fileTree(include: ['*.jar'], dir: "${rootProject.rootDir}/libs/koin-android")
implementation fileTree(include: ['*.aar'], 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 "cash.z.ecc.android:kotlin-bip39:1.0.4"
implementation fileTree(include: ['kotlin-bip39-1.0.2.jar'], dir: "${rootProject.rootDir}/libs") implementation fileTree(include: ['kotlin-bip39-jvm-1.0.4.jar'], dir: "${rootProject.rootDir}/libs")
/** /**
* Test Dependencies (do not concern the AOSP build) * Test Dependencies (do not concern the AOSP build)
@ -138,10 +138,11 @@ dependencies {
// anything less than 'implementation' fails tests run with gradlew // anything less than 'implementation' fails tests run with gradlew
testImplementation rootProject.ext.aosp_libs testImplementation rootProject.ext.aosp_libs
testImplementation 'androidx.test.ext:junit:1.1.3' 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 // https://github.com/robolectric/robolectric/issues/5245
exclude group: "com.google.auto.service", module: "auto-service" 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-api:$junit5_version"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5_version" testImplementation "org.junit.jupiter:junit-jupiter-params:$junit5_version"
testImplementation "io.mockk:mockk:$mockk_version" testImplementation "io.mockk:mockk:$mockk_version"
@ -159,9 +160,6 @@ apply from: "${rootProject.rootDir}/gradle/ktlint.gradle"
gradle.projectsEvaluated { gradle.projectsEvaluated {
tasks.withType(JavaCompile) { 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') 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 <activity
android:name="com.stevesoltys.seedvault.settings.SettingsActivity" android:name="com.stevesoltys.seedvault.settings.SettingsActivity"
android:exported="true" android:exported="true"
android:permission="" tools:remove="android:permission" />
tools:replace="android:permission" />
<activity <activity
android:name="com.stevesoltys.seedvault.restore.RestoreActivity" android:name="com.stevesoltys.seedvault.restore.RestoreActivity"
android:exported="true" android:exported="true"
android:permission="" tools:remove="android:permission" />
tools:replace="android:permission" />
</application> </application>
</manifest> </manifest>

View file

@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.stevesoltys.seedvault" package="com.stevesoltys.seedvault"
android:versionCode="31000301" android:versionCode="33000301"
android:versionName="12-3.0"> android:versionName="13-3.1">
<!-- <!--
The version code is the targeted SDK_VERSION plus 6 digits for our own version code. 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. 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 --> <!-- This is needed to check for internet access when backup is stored on network storage -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <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 <uses-permission
android:name="android.permission.MANAGE_DOCUMENTS" android:name="android.permission.MANAGE_DOCUMENTS"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
@ -50,6 +54,16 @@
<!-- Used to authenticate saving a new recovery code --> <!-- Used to authenticate saving a new recovery code -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <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 used to open settings -->
<permission <permission
android:name="com.stevesoltys.seedvault.OPEN_SETTINGS" android:name="com.stevesoltys.seedvault.OPEN_SETTINGS"
@ -60,11 +74,6 @@
android:name="com.stevesoltys.seedvault.RESTORE_BACKUP" android:name="com.stevesoltys.seedvault.RESTORE_BACKUP"
android:protectionLevel="system|signature" /> 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 <application
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="false"

View file

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

View file

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

View file

@ -1,15 +1,35 @@
package com.stevesoltys.seedvault.settings package com.stevesoltys.seedvault.settings
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.permitDiskReads 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() { 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
permitDiskReads { permitDiskReads {
setPreferencesFromResource(R.xml.settings_expert, rootKey) 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() { override fun onStart() {

View file

@ -105,7 +105,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton(R.string.settings_backup_apk_dialog_disable) { dialog, _ -> .setNegativeButton(R.string.settings_backup_apk_dialog_disable) { dialog, _ ->
apkBackup.isChecked = enable apkBackup.isChecked = false
dialog.dismiss() dialog.dismiss()
} }
.show() .show()
@ -130,14 +130,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.lastBackupTime.observe(viewLifecycleOwner, { time -> viewModel.lastBackupTime.observe(viewLifecycleOwner) { time ->
setAppBackupStatusSummary(time) setAppBackupStatusSummary(time)
}) }
val backupFiles: Preference = findPreference("backup_files")!! val backupFiles: Preference = findPreference("backup_files")!!
viewModel.filesSummary.observe(viewLifecycleOwner, { summary -> viewModel.filesSummary.observe(viewLifecycleOwner) { summary ->
backupFiles.summary = summary backupFiles.summary = summary
}) }
} }
override fun onStart() { override fun onStart() {
@ -160,10 +160,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
if (resources.getBoolean(R.bool.show_restore_in_settings)) { if (resources.getBoolean(R.bool.show_restore_in_settings)) {
menuRestore?.isVisible = true menuRestore?.isVisible = true
} }
viewModel.backupPossible.observe(viewLifecycleOwner, { possible -> viewModel.backupPossible.observe(viewLifecycleOwner) { possible ->
menuBackupNow?.isEnabled = possible menuBackupNow?.isEnabled = possible
menuRestore?.isEnabled = possible menuRestore?.isEnabled = possible
}) }
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { 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.NetworkCapabilities
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.Uri import android.net.Uri
import android.os.Process.myUid
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
@ -35,8 +36,11 @@ import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.calyxos.backup.storage.api.StorageBackup import org.calyxos.backup.storage.api.StorageBackup
import org.calyxos.backup.storage.backup.BackupJobService import org.calyxos.backup.storage.backup.BackupJobService
import java.io.IOException
import java.lang.Runtime.getRuntime
import java.util.concurrent.TimeUnit.HOURS import java.util.concurrent.TimeUnit.HOURS
private const val TAG = "SettingsViewModel" private const val TAG = "SettingsViewModel"
@ -193,9 +197,9 @@ internal class SettingsViewModel(
@UiThread @UiThread
fun loadFilesSummary() = viewModelScope.launch { fun loadFilesSummary() = viewModelScope.launch {
val uriSummary = storageBackup.getUriSummaryString() val uriSummary = storageBackup.getUriSummaryString()
_filesSummary.value = if (uriSummary.isEmpty()) { _filesSummary.value = uriSummary.ifEmpty {
app.getString(R.string.settings_backup_files_summary) app.getString(R.string.settings_backup_files_summary)
} else uriSummary }
} }
/** /**
@ -233,4 +237,28 @@ internal class SettingsViewModel(
BackupJobService.cancelJob(app) 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, streamGetter: suspend (name: String) -> OutputStream,
): List<ApkSplit> { ): List<ApkSplit> {
check(packageInfo.splitNames != null) 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) { check(packageInfo.splitNames.size == splitSourceDirs.size) {
"Size Mismatch! ${packageInfo.splitNames.size} != ${splitSourceDirs.size} " + "Size Mismatch! ${packageInfo.splitNames.size} != ${splitSourceDirs.size} " +
"splitNames is ${packageInfo.splitNames.toList()}, " + "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 // hook in here to back up APKs of apps that are otherwise not allowed for backup
if (isPmBackup && settingsManager.canDoBackupNow()) { 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 result

View file

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

View file

@ -2,7 +2,6 @@ package com.stevesoltys.seedvault.ui
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build.VERSION.SDK_INT
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.INVISIBLE import android.view.View.INVISIBLE
@ -40,12 +39,10 @@ internal abstract class AppViewHolder(protected val v: View) : RecyclerView.View
appInfo.visibility = GONE appInfo.visibility = GONE
appStatus.visibility = INVISIBLE appStatus.visibility = INVISIBLE
progressBar.visibility = VISIBLE progressBar.visibility = VISIBLE
if (SDK_INT >= 30) { progressBar.stateDescription = context.getString(
progressBar.stateDescription = context.getString( if (isRestore) R.string.restore_restoring
if (isRestore) R.string.restore_restoring else R.string.backup_app_in_progress
else R.string.backup_app_in_progress )
)
}
} else { } else {
appStatus.visibility = VISIBLE appStatus.visibility = VISIBLE
progressBar.visibility = INVISIBLE 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 currentBackupPackage The name of the package that now being backed up.
* @param backupProgress Current progress of backup for the package. * @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) showProgressNotification(currentBackupPackage)
} }
@ -57,7 +57,7 @@ internal class NotificationBackupObserver(
* that was initialized * that was initialized
* @param status Zero on success; a nonzero error code if the backup operation failed. * @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)) { if (isLoggable(TAG, INFO)) {
Log.i(TAG, "Completed. Target: $target, status: $status") Log.i(TAG, "Completed. Target: $target, status: $status")
} }
@ -81,8 +81,8 @@ internal class NotificationBackupObserver(
nm.onBackupFinished(success, numBackedUp) nm.onBackupFinished(success, numBackedUp)
} }
private fun showProgressNotification(packageName: String) { private fun showProgressNotification(packageName: String?) {
if (currentPackage == packageName) return if (packageName == null || currentPackage == packageName) return
if (isLoggable(TAG, INFO)) { if (isLoggable(TAG, INFO)) {
"Showing progress notification for $currentPackage $numPackages/$expectedPackages".let { "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.MenuItem
import android.view.WindowManager.LayoutParams.FLAG_SECURE import android.view.WindowManager.LayoutParams.FLAG_SECURE
import com.stevesoltys.seedvault.R import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.isDebugBuild
import com.stevesoltys.seedvault.ui.BackupActivity import com.stevesoltys.seedvault.ui.BackupActivity
import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE import com.stevesoltys.seedvault.ui.INTENT_EXTRA_IS_RESTORE
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@ -15,7 +16,7 @@ class RecoveryCodeActivity : BackupActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.addFlags(FLAG_SECURE) if (!isDebugBuild()) window.addFlags(FLAG_SECURE)
setContentView(R.layout.activity_recovery_code) 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.BIOMETRIC_STRONG
import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL import android.hardware.biometrics.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.BiometricPrompt
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle import android.os.Bundle
import android.os.CancellationSignal import android.os.CancellationSignal
import android.view.LayoutInflater import android.view.LayoutInflater
@ -22,7 +21,6 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat.getMainExecutor import androidx.core.content.ContextCompat.getMainExecutor
@ -122,9 +120,9 @@ class RecoveryCodeInputFragment : Fragment() {
newCodeButton.visibility = if (forStoringNewCode) GONE else VISIBLE newCodeButton.visibility = if (forStoringNewCode) GONE else VISIBLE
newCodeButton.setOnClickListener { generateNewCode() } newCodeButton.setOnClickListener { generateNewCode() }
viewModel.existingCodeChecked.observeEvent(viewLifecycleOwner, { verified -> viewModel.existingCodeChecked.observeEvent(viewLifecycleOwner) { verified ->
onExistingCodeChecked(verified) onExistingCodeChecked(verified)
}) }
if (forStoringNewCode && isDebugBuild() && !viewModel.isRestore) debugPreFill() if (forStoringNewCode && isDebugBuild() && !viewModel.isRestore) debugPreFill()
} }
@ -147,7 +145,7 @@ class RecoveryCodeInputFragment : Fragment() {
} }
if (forStoringNewCode) { if (forStoringNewCode) {
val keyguardManager = requireContext().getSystemService(KeyguardManager::class.java) 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 // if we have a lock-screen secret, we can ask for it before storing the code
storeNewCodeAfterAuth(input) storeNewCodeAfterAuth(input)
} else { } else {
@ -159,7 +157,6 @@ class RecoveryCodeInputFragment : Fragment() {
} }
} }
@RequiresApi(30)
private fun storeNewCodeAfterAuth(input: List<CharSequence>) { private fun storeNewCodeAfterAuth(input: List<CharSequence>) {
val context = requireContext() val context = requireContext()
val biometricPrompt = BiometricPrompt.Builder(context) 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_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_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_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_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> <string name="settings_expert_logcat_error">Could not save app log</string>
<!-- Storage Location --> <!-- Storage Location -->
<string name="storage_fragment_backup_title">Choose where to store backups</string> <string name="storage_fragment_backup_title">Choose where to store backups</string>
<string name="storage_fragment_restore_title">Where to find your 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:key="unlimited_quota"
android:summary="@string/settings_expert_quota_summary" android:summary="@string/settings_expert_quota_summary"
android:title="@string/settings_expert_quota_title" /> 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") @Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@Config( @Config(
sdk = [29], // robolectric does not support 30, yet sdk = [32], // robolectric does not support 33, yet
application = TestApp::class application = TestApp::class
) )
class MetadataManagerTest { class MetadataManagerTest {

View file

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

View file

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

View file

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

View file

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

View file

@ -37,12 +37,13 @@ internal abstract class TransportTest {
protected val sigInfo: SigningInfo = mockk() protected val sigInfo: SigningInfo = mockk()
protected val token = Random.nextLong() protected val token = Random.nextLong()
protected val applicationInfo = mockk<ApplicationInfo> {
flags = FLAG_ALLOW_BACKUP or FLAG_INSTALLED
}
protected val packageInfo = PackageInfo().apply { protected val packageInfo = PackageInfo().apply {
packageName = "org.example" packageName = "org.example"
longVersionCode = Random.nextLong() longVersionCode = Random.nextLong()
applicationInfo = ApplicationInfo().apply { applicationInfo = this@TransportTest.applicationInfo
flags = FLAG_ALLOW_BACKUP or FLAG_INSTALLED
}
signingInfo = sigInfo signingInfo = sigInfo
} }
protected val pmPackageInfo = PackageInfo().apply { 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_OK
import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED import android.app.backup.BackupTransport.TRANSPORT_PACKAGE_REJECTED
import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED import android.app.backup.BackupTransport.TRANSPORT_QUOTA_EXCEEDED
import android.content.pm.ApplicationInfo
import android.content.pm.ApplicationInfo.FLAG_STOPPED import android.content.pm.ApplicationInfo.FLAG_STOPPED
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.net.Uri import android.net.Uri
@ -399,7 +398,9 @@ internal class BackupCoordinatorTest : BackupTest() {
PackageInfo().apply { PackageInfo().apply {
packageName = "org.example.2" packageName = "org.example.2"
// the second package does not get backed up, because it is stopped // 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() val packageMetadata: PackageMetadata = mockk()

View file

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

View file

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

View file

@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.calyxos.backup.contacts" package="org.calyxos.backup.contacts"
android:versionCode="31000301" android:versionCode="33000301"
android:versionName="12-3.0"> android:versionName="13-3.1">
<!-- <!--
The version code is the targeted SDK_VERSION plus 6 digits for our own version code. 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. 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 { ext {
// https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-12.0.0_r2/current/androidx/Android.bp#2943 // https://android.googlesource.com/platform/prebuilts/sdk/+/refs/tags/android-13.0.0_r3/current/androidx/Android.bp#3901
ext.room_version = "2.3.0-beta02" ext.room_version = "2.4.0-alpha05"
// https://android.googlesource.com/platform/external/protobuf/+/refs/tags/android-12.0.0_r2/java/pom.xml#7 // https://android.googlesource.com/platform/external/protobuf/+/refs/tags/android-13.0.0_r3/java/pom.xml#7
ext.protobuf_version = "3.9.1" 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" junit4_version = "4.13.2"
junit5_version = "5.5.2" // careful, upgrading this can change a Cipher's IV size in tests!? junit5_version = "5.7.2" // careful, upgrading this can change a Cipher's IV size in tests!?
mockk_version = "1.12.0" mockk_version = "1.12.3"
espresso_version = "3.4.0" espresso_version = "3.4.0"
} }
@ -37,63 +39,63 @@ ext.kotlin_libs = [
], ],
coroutines: [ coroutines: [
dependencies.create('org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm') { 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 // https://android.googlesource.com/platform/external/kotlinx.coroutines/+/refs/tags/android-13.0.0_r3/CHANGES.md
version { strictly '1.4.2' } version { strictly '1.5.2' }
}, },
dependencies.create('org.jetbrains.kotlinx:kotlinx-coroutines-android') { 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 // https://android.googlesource.com/platform/external/kotlinx.coroutines/+/refs/tags/android-13.0.0_r3/CHANGES.md
version { strictly '1.3.0' } version { strictly '1.5.2' }
}, },
], ],
] ]
ext.std_libs = [ ext.std_libs = [
androidx_core: [ 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') { 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') { 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') { 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') { 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') { 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') { 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') { 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') { androidx_constraintlayout: dependencies.create('androidx.constraintlayout:constraintlayout') {
version { strictly '2.0.0-beta7' } 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') { 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') { 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 = [ 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 = [ ext.storage_libs = [
@ -103,7 +105,9 @@ ext.storage_libs = [
com_google_protobuf_javalite: dependencies.create('com.google.protobuf:protobuf-javalite') { com_google_protobuf_javalite: dependencies.create('com.google.protobuf:protobuf-javalite') {
version { strictly "$protobuf_version" } version { strictly "$protobuf_version" }
}, },
// https://github.com/google/tink/releases
com_google_crypto_tink_android: dependencies.create('com.google.crypto.tink:tink-android') { 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 distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip zipStoreBase=GRADLE_USER_HOME
distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302

View file

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

View file

@ -1,11 +1,11 @@
android_library_import { android_library_import {
name: "seedvault-lib-koin-android", name: "seedvault-lib-koin-android",
aars: ["koin-android-3.1.2.aar"], aars: ["koin-android-3.2.0.aar"],
sdk_version: "current", sdk_version: "current",
} }
java_import { java_import {
name: "seedvault-lib-koin-core-jvm", 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", 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 { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = '1.8' jvmTarget = JavaVersion.VERSION_11.toString()
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
} }
lintOptions { lintOptions {

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

View file

@ -3,13 +3,6 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="org.calyxos.backup.storage"> 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 <uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE" android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />

View file

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

View file

@ -8,7 +8,7 @@ import org.calyxos.backup.storage.measure
import org.calyxos.backup.storage.plugin.SnapshotRetriever import org.calyxos.backup.storage.plugin.SnapshotRetriever
import java.io.IOException import java.io.IOException
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.util.concurrent.TimeUnit.MILLISECONDS import kotlin.time.DurationUnit.MILLISECONDS
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import kotlin.time.toDuration 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.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import kotlin.time.ExperimentalTime
private val folderRegex = Regex("^[a-f0-9]{16}\\.sv$") private val folderRegex = Regex("^[a-f0-9]{16}\\.sv$")
private val chunkFolderRegex = Regex("[a-f0-9]{2}") 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. * Chunk folders will get cached in the given [chunkFolders] for faster access.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
@OptIn(ExperimentalTime::class)
private suspend fun populateChunkFolders( private suspend fun populateChunkFolders(
folder: DocumentFile, folder: DocumentFile,
chunkFolders: HashMap<String, DocumentFile>, chunkFolders: HashMap<String, DocumentFile>,
@ -126,6 +128,7 @@ public abstract class SafStoragePlugin(
} }
@Throws(IOException::class) @Throws(IOException::class)
@OptIn(ExperimentalTime::class)
private fun createMissingChunkFolders( private fun createMissingChunkFolders(
root: DocumentFile, root: DocumentFile,
chunkFolders: HashMap<String, DocumentFile>, chunkFolders: HashMap<String, DocumentFile>,

View file

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

View file

@ -4,7 +4,6 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Environment.getExternalStorageDirectory import android.os.Environment.getExternalStorageDirectory
import android.provider.MediaStore.MediaColumns import android.provider.MediaStore.MediaColumns
import android.util.Log import android.util.Log
@ -39,13 +38,7 @@ internal class FileRestore(
val finalTag: String val finalTag: String
when { when {
file.mediaFile != null -> { file.mediaFile != null -> {
bytes = if (SDK_INT < 30) { bytes = restoreFile(file.mediaFile, streamWriter)
// 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)
}
finalTag = "M$tag" finalTag = "M$tag"
} }
file.docFile != null -> { file.docFile != null -> {
@ -112,10 +105,7 @@ internal class FileRestore(
// changing owner requires backup permission // changing owner requires backup permission
put(MediaColumns.OWNER_PACKAGE_NAME, mediaFile.ownerPackageName) put(MediaColumns.OWNER_PACKAGE_NAME, mediaFile.ownerPackageName)
put(MediaColumns.IS_PENDING, 1) put(MediaColumns.IS_PENDING, 1)
if (SDK_INT >= 30) { put(MediaColumns.IS_FAVORITE, if (mediaFile.isFavorite) 1 else 0)
val isFavorite = if (mediaFile.isFavorite) 1 else 0
put(MediaColumns.IS_FAVORITE, isFavorite)
}
} }
val contentUri = MediaType.fromBackupMediaType(mediaFile.type).contentUri val contentUri = MediaType.fromBackupMediaType(mediaFile.type).contentUri
val uri = contentResolver.insert(contentUri, contentValues)!! val uri = contentResolver.insert(contentUri, contentValues)!!

View file

@ -52,6 +52,7 @@ internal class Restore(
MultiChunkRestore(context, storagePlugin, fileRestore, streamCrypto, streamKey) MultiChunkRestore(context, storagePlugin, fileRestore, streamCrypto, streamKey)
} }
@OptIn(ExperimentalTime::class)
fun getBackupSnapshots(): Flow<SnapshotResult> = flow { fun getBackupSnapshots(): Flow<SnapshotResult> = flow {
val numSnapshots: Int val numSnapshots: Int
val time = measure { val time = measure {
@ -138,7 +139,7 @@ internal class Restore(
Log.e(TAG, "Restoring ${split.multiChunkFiles.size} multi chunks took $multiChunkDuration.") Log.e(TAG, "Restoring ${split.multiChunkFiles.size} multi chunks took $multiChunkDuration.")
val totalDuration = smallFilesDuration + singleChunkDuration + multiChunkDuration val totalDuration = smallFilesDuration + singleChunkDuration + multiChunkDuration
observer?.onRestoreComplete(totalDuration.toLongMilliseconds()) observer?.onRestoreComplete(totalDuration.inWholeMilliseconds)
Log.e(TAG, "Restored $restoredFiles/$filesTotal files.") 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.content.MediaFile
import org.calyxos.backup.storage.db.UriStore import org.calyxos.backup.storage.db.UriStore
import org.calyxos.backup.storage.measure import org.calyxos.backup.storage.measure
import kotlin.time.ExperimentalTime
internal class FileScannerResult( internal class FileScannerResult(
val smallFiles: List<ContentFile>, val smallFiles: List<ContentFile>,
@ -30,6 +31,7 @@ internal class FileScanner(
private const val FILES_LARGE = "large" private const val FILES_LARGE = "large"
} }
@OptIn(ExperimentalTime::class)
fun getFiles(): FileScannerResult { fun getFiles(): FileScannerResult {
// scan both APIs // scan both APIs
val mediaFiles = ArrayList<ContentFile>() val mediaFiles = ArrayList<ContentFile>()

View file

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