Merge pull request #436 from grote/android13-upgrades-minSdk

More work for Android13
This commit is contained in:
Torsten Grote 2022-08-23 11:56:55 -03:00 committed by GitHub
commit fca09ccb9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 76 additions and 123 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>

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

@ -44,6 +44,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"
@ -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)

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,6 +16,8 @@
<!-- 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" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- This is needed to retrieve the available storage roots --> <!-- This is needed to retrieve the available storage roots -->
<uses-permission <uses-permission
android:name="android.permission.MANAGE_DOCUMENTS" android:name="android.permission.MANAGE_DOCUMENTS"

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,11 +80,9 @@ 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)
appStatus.visibility = VISIBLE appStatus.visibility = VISIBLE

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

@ -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

@ -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

@ -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

@ -22,7 +22,7 @@ buildscript {
ext { ext {
buildToolsVersion = '33.0.0' buildToolsVersion = '33.0.0'
compileSdkVersion = 33 compileSdkVersion = 33
minSdkVersion = 29 minSdkVersion = 32
targetSdkVersion = 33 targetSdkVersion = 33
} }

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

@ -3,9 +3,11 @@ ext {
ext.room_version = "2.4.0-alpha05" 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 // 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"
} }
@ -93,7 +95,7 @@ ext.std_libs = [
] ]
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,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

@ -7,5 +7,6 @@
<permission name="android.permission.INTERACT_ACROSS_USERS_FULL"/> <permission name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
<permission name="android.permission.WRITE_SECURE_SETTINGS"/> <permission name="android.permission.WRITE_SECURE_SETTINGS"/>
<permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/> <permission name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
<permission name="android.permission.POST_NOTIFICATIONS"/>
</privapp-permissions> </privapp-permissions>
</permissions> </permissions>

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) startActivity(i)
false return 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
}
}
} }
} }

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,17 +38,15 @@ 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)}")
sb.appendLine("Media files smaller than 500 KB: ${mediaFilesSmallerThan(500 * 1024)}") sb.appendLine("Media files smaller than 500 KB: ${mediaFilesSmallerThan(500 * 1024)}")

View file

@ -39,6 +39,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 {

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

@ -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

@ -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()
} }