Enable automatic coding style linting with ktlint (also on CI)
This way the coding style is guaranteed to stay consistent.
This commit is contained in:
parent
53937bda2f
commit
6c531066e7
31 changed files with 167 additions and 70 deletions
4
.editorconfig
Normal file
4
.editorconfig
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[*.{kt,kts}]
|
||||||
|
indent_size=4
|
||||||
|
insert_final_newline=true
|
||||||
|
max_line_length=100
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,6 +9,7 @@ out/
|
||||||
lib/
|
lib/
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/runConfigurations*
|
!.idea/runConfigurations*
|
||||||
|
!.idea/inspectionProfiles*
|
||||||
!.idea/codeStyles*
|
!.idea/codeStyles*
|
||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
|
|
|
@ -131,6 +131,16 @@
|
||||||
</codeStyleSettings>
|
</codeStyleSettings>
|
||||||
<codeStyleSettings language="kotlin">
|
<codeStyleSettings language="kotlin">
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<option name="RIGHT_MARGIN" value="100" />
|
||||||
|
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||||
|
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="PARAMETER_ANNOTATION_WRAP" value="1" />
|
||||||
|
<option name="VARIABLE_ANNOTATION_WRAP" value="1" />
|
||||||
|
<option name="ENUM_CONSTANTS_WRAP" value="1" />
|
||||||
<indentOptions>
|
<indentOptions>
|
||||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
</indentOptions>
|
</indentOptions>
|
||||||
|
|
10
.idea/inspectionProfiles/Project_Default.xml
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="AndroidLintMangledCRLF" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="InconsistentLineSeparators" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="KotlinUnusedImport" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="LongLine" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="RedundantSemicolon" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
|
@ -24,7 +24,7 @@ before_cache:
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
||||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
||||||
|
|
||||||
script: ./gradlew check assemble
|
script: ./gradlew check assemble ktlintCheck
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import groovy.xml.XmlUtil
|
import groovy.xml.XmlUtil
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
plugins {
|
||||||
apply plugin: 'kotlin-android'
|
id "com.android.application"
|
||||||
|
id "kotlin-android"
|
||||||
|
id "org.jlleitschuh.gradle.ktlint" version "9.4.0"
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
@ -74,6 +77,17 @@ android {
|
||||||
|
|
||||||
apply from: '../gradle/dependencies.gradle'
|
apply from: '../gradle/dependencies.gradle'
|
||||||
|
|
||||||
|
ktlint {
|
||||||
|
version = "0.36.0" // https://github.com/pinterest/ktlint/issues/764
|
||||||
|
android = true
|
||||||
|
enableExperimentalRules = false
|
||||||
|
verbose = true
|
||||||
|
disabledRules = [
|
||||||
|
"import-ordering",
|
||||||
|
"no-blank-line-before-rbrace",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
gradle.projectsEvaluated {
|
gradle.projectsEvaluated {
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
|
if (JavaVersion.current() >= JavaVersion.VERSION_1_9) {
|
||||||
|
@ -92,9 +106,14 @@ preBuild.doLast {
|
||||||
def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
|
def jdkNode = parsedXml.component[1].orderEntry.find { it.'@type' == 'jdk' }
|
||||||
parsedXml.component[1].remove(jdkNode)
|
parsedXml.component[1].remove(jdkNode)
|
||||||
|
|
||||||
def sdkString = "Android API " + android.compileSdkVersion.substring("android-".length()) + " Platform"
|
def apiString = android.compileSdkVersion.substring("android-".length())
|
||||||
|
def sdkString = "Android API " + apiString + " Platform"
|
||||||
//noinspection GroovyResultOfObjectAllocationIgnored // the note gets inserted
|
//noinspection GroovyResultOfObjectAllocationIgnored // the note gets inserted
|
||||||
new Node(parsedXml.component[1], 'orderEntry', ['type': 'jdk', 'jdkName': sdkString, 'jdkType': 'Android SDK'])
|
new Node(parsedXml.component[1], 'orderEntry', [
|
||||||
|
'type' : 'jdk',
|
||||||
|
'jdkName': sdkString,
|
||||||
|
'jdkType': 'Android SDK'
|
||||||
|
])
|
||||||
XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
|
XmlUtil.serialize(parsedXml, new FileOutputStream(imlFile))
|
||||||
|
|
||||||
} catch (NullPointerException | FileNotFoundException ex) {
|
} catch (NullPointerException | FileNotFoundException ex) {
|
||||||
|
|
|
@ -56,7 +56,7 @@ class App : Application() {
|
||||||
cryptoModule,
|
cryptoModule,
|
||||||
headerModule,
|
headerModule,
|
||||||
metadataModule,
|
metadataModule,
|
||||||
documentsProviderModule, // storage plugin
|
documentsProviderModule, // storage plugin
|
||||||
backupModule,
|
backupModule,
|
||||||
restoreModule,
|
restoreModule,
|
||||||
appModule
|
appModule
|
||||||
|
|
|
@ -69,7 +69,9 @@ abstract class UsbMonitor : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val action = intent.action ?: return
|
val action = intent.action ?: return
|
||||||
if (intent.action == ACTION_USB_DEVICE_ATTACHED || intent.action == ACTION_USB_DEVICE_DETACHED) {
|
if (intent.action == ACTION_USB_DEVICE_ATTACHED ||
|
||||||
|
intent.action == ACTION_USB_DEVICE_DETACHED
|
||||||
|
) {
|
||||||
val device = intent.extras?.getParcelable<UsbDevice>(EXTRA_DEVICE) ?: return
|
val device = intent.extras?.getParcelable<UsbDevice>(EXTRA_DEVICE) ?: return
|
||||||
Log.d(TAG, "New USB mass-storage device attached.")
|
Log.d(TAG, "New USB mass-storage device attached.")
|
||||||
device.log()
|
device.log()
|
||||||
|
|
|
@ -115,7 +115,8 @@ internal class CryptoImpl(
|
||||||
val cipher = cipherFactory.createEncryptionCipher()
|
val cipher = cipherFactory.createEncryptionCipher()
|
||||||
|
|
||||||
check(cipher.getOutputSize(cleartext.size) <= MAX_SEGMENT_LENGTH) {
|
check(cipher.getOutputSize(cleartext.size) <= MAX_SEGMENT_LENGTH) {
|
||||||
"Cipher's output size ${cipher.getOutputSize(cleartext.size)} is larger than maximum segment length ($MAX_SEGMENT_LENGTH)"
|
"Cipher's output size ${cipher.getOutputSize(cleartext.size)} is larger" +
|
||||||
|
"than maximum segment length ($MAX_SEGMENT_LENGTH)"
|
||||||
}
|
}
|
||||||
encryptSegment(cipher, outputStream, cleartext)
|
encryptSegment(cipher, outputStream, cleartext)
|
||||||
}
|
}
|
||||||
|
@ -162,9 +163,9 @@ internal class CryptoImpl(
|
||||||
"expected '$expectedPackageName'."
|
"expected '$expectedPackageName'."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (header.key != expectedKey) {
|
if (header.key != expectedKey) throw SecurityException(
|
||||||
throw SecurityException("Invalid key '${header.key}' in header, expected '$expectedKey'.")
|
"Invalid key '${header.key}' in header, expected '$expectedKey'."
|
||||||
}
|
)
|
||||||
|
|
||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
|
@ -190,9 +191,9 @@ internal class CryptoImpl(
|
||||||
@Throws(EOFException::class, IOException::class, SecurityException::class)
|
@Throws(EOFException::class, IOException::class, SecurityException::class)
|
||||||
private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
|
private fun decryptSegment(inputStream: InputStream, maxSegmentLength: Int): ByteArray {
|
||||||
val segmentHeader = headerReader.readSegmentHeader(inputStream)
|
val segmentHeader = headerReader.readSegmentHeader(inputStream)
|
||||||
if (segmentHeader.segmentLength > maxSegmentLength) {
|
if (segmentHeader.segmentLength > maxSegmentLength) throw SecurityException(
|
||||||
throw SecurityException("Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength")
|
"Segment length too long: ${segmentHeader.segmentLength} > $maxSegmentLength"
|
||||||
}
|
)
|
||||||
|
|
||||||
val buffer = ByteArray(segmentHeader.segmentLength.toInt())
|
val buffer = ByteArray(segmentHeader.segmentLength.toInt())
|
||||||
val bytesRead = inputStream.read(buffer)
|
val bytesRead = inputStream.read(buffer)
|
||||||
|
|
|
@ -22,7 +22,9 @@ data class VersionHeader(
|
||||||
"Package $packageName has name longer than $MAX_PACKAGE_LENGTH_SIZE"
|
"Package $packageName has name longer than $MAX_PACKAGE_LENGTH_SIZE"
|
||||||
}
|
}
|
||||||
key?.let {
|
key?.let {
|
||||||
check(key.length <= MAX_KEY_LENGTH_SIZE) { "Key $key is longer than $MAX_KEY_LENGTH_SIZE" }
|
check(key.length <= MAX_KEY_LENGTH_SIZE) {
|
||||||
|
"Key $key is longer than $MAX_KEY_LENGTH_SIZE"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,15 +33,21 @@ internal class HeaderReaderImpl : HeaderReader {
|
||||||
|
|
||||||
val packageLength = buffer.short.toInt()
|
val packageLength = buffer.short.toInt()
|
||||||
if (packageLength <= 0) throw SecurityException("Invalid package length: $packageLength")
|
if (packageLength <= 0) throw SecurityException("Invalid package length: $packageLength")
|
||||||
if (packageLength > MAX_PACKAGE_LENGTH_SIZE) throw SecurityException("Too large package length: $packageLength")
|
if (packageLength > MAX_PACKAGE_LENGTH_SIZE) throw SecurityException(
|
||||||
if (packageLength > buffer.remaining()) throw SecurityException("Not enough bytes for package name")
|
"Too large package length: $packageLength"
|
||||||
|
)
|
||||||
|
if (packageLength > buffer.remaining()) throw SecurityException(
|
||||||
|
"Not enough bytes for package name"
|
||||||
|
)
|
||||||
val packageName = ByteArray(packageLength)
|
val packageName = ByteArray(packageLength)
|
||||||
.apply { buffer.get(this) }
|
.apply { buffer.get(this) }
|
||||||
.toString(Utf8)
|
.toString(Utf8)
|
||||||
|
|
||||||
val keyLength = buffer.short.toInt()
|
val keyLength = buffer.short.toInt()
|
||||||
if (keyLength < 0) throw SecurityException("Invalid key length: $keyLength")
|
if (keyLength < 0) throw SecurityException("Invalid key length: $keyLength")
|
||||||
if (keyLength > MAX_KEY_LENGTH_SIZE) throw SecurityException("Too large key length: $keyLength")
|
if (keyLength > MAX_KEY_LENGTH_SIZE) throw SecurityException(
|
||||||
|
"Too large key length: $keyLength"
|
||||||
|
)
|
||||||
if (keyLength > buffer.remaining()) throw SecurityException("Not enough bytes for key")
|
if (keyLength > buffer.remaining()) throw SecurityException("Not enough bytes for key")
|
||||||
val key = if (keyLength == 0) null else ByteArray(keyLength)
|
val key = if (keyLength == 0) null else ByteArray(keyLength)
|
||||||
.apply { buffer.get(this) }
|
.apply { buffer.get(this) }
|
||||||
|
|
|
@ -80,7 +80,8 @@ class MetadataManager(
|
||||||
"APK backup returned version null"
|
"APK backup returned version null"
|
||||||
}
|
}
|
||||||
check(it.version == null || it.version < packageMetadata.version) {
|
check(it.version == null || it.version < packageMetadata.version) {
|
||||||
"APK backup backed up the same or a smaller version: was ${it.version} is ${packageMetadata.version}"
|
"APK backup backed up the same or a smaller version:" +
|
||||||
|
"was ${it.version} is ${packageMetadata.version}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val oldPackageMetadata = metadata.packageMetadataMap[packageName]
|
val oldPackageMetadata = metadata.packageMetadataMap[packageName]
|
||||||
|
|
|
@ -78,9 +78,9 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val token = meta.getLong(JSON_METADATA_TOKEN)
|
val token = meta.getLong(JSON_METADATA_TOKEN)
|
||||||
if (expectedToken != null && token != expectedToken) {
|
if (expectedToken != null && token != expectedToken) throw SecurityException(
|
||||||
throw SecurityException("Invalid token '$token' in metadata, expected '$expectedToken'.")
|
"Invalid token '$token' in metadata, expected '$expectedToken'."
|
||||||
}
|
)
|
||||||
// get package metadata
|
// get package metadata
|
||||||
val packageMetadataMap = PackageMetadataMap()
|
val packageMetadataMap = PackageMetadataMap()
|
||||||
for (packageName in json.keys()) {
|
for (packageName in json.keys()) {
|
||||||
|
|
|
@ -15,7 +15,9 @@ internal const val REQUEST_CODE_UNINSTALL = 4576841
|
||||||
class RestoreErrorBroadcastReceiver : BroadcastReceiver() {
|
class RestoreErrorBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
// using KoinComponent would crash robolectric tests :(
|
// using KoinComponent would crash robolectric tests :(
|
||||||
private val notificationManager: BackupNotificationManager by lazy { get().get<BackupNotificationManager>() }
|
private val notificationManager: BackupNotificationManager by lazy {
|
||||||
|
get().get<BackupNotificationManager>()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
if (intent.action != ACTION_RESTORE_ERROR_UNINSTALL) return
|
if (intent.action != ACTION_RESTORE_ERROR_UNINSTALL) return
|
||||||
|
@ -24,7 +26,7 @@ class RestoreErrorBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!!
|
val packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME)!!
|
||||||
|
|
||||||
@Suppress("DEPRECATION") // the alternative doesn't work for us
|
@Suppress("DEPRECATION") // the alternative doesn't work for us
|
||||||
val i = Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
|
val i = Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
|
||||||
data = "package:$packageName".toUri()
|
data = "package:$packageName".toUri()
|
||||||
flags = FLAG_ACTIVITY_NEW_TASK
|
flags = FLAG_ACTIVITY_NEW_TASK
|
||||||
|
|
|
@ -186,7 +186,9 @@ internal class RestoreViewModel(
|
||||||
// we need to start a new session and retrieve the restore sets before starting the restore
|
// we need to start a new session and retrieve the restore sets before starting the restore
|
||||||
val restoreSetResult = getAvailableRestoreSets()
|
val restoreSetResult = getAvailableRestoreSets()
|
||||||
if (restoreSetResult.hasError()) {
|
if (restoreSetResult.hasError()) {
|
||||||
mRestoreBackupResult.postValue(RestoreBackupResult(app.getString(R.string.restore_finished_error)))
|
mRestoreBackupResult.postValue(
|
||||||
|
RestoreBackupResult(app.getString(R.string.restore_finished_error))
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +198,9 @@ internal class RestoreViewModel(
|
||||||
if (restoreAllResult != 0) {
|
if (restoreAllResult != 0) {
|
||||||
if (session == null) Log.e(TAG, "session was null")
|
if (session == null) Log.e(TAG, "session was null")
|
||||||
else Log.e(TAG, "restoreAll() returned non-zero value")
|
else Log.e(TAG, "restoreAll() returned non-zero value")
|
||||||
mRestoreBackupResult.postValue(RestoreBackupResult(app.getString(R.string.restore_finished_error)))
|
mRestoreBackupResult.postValue(
|
||||||
|
RestoreBackupResult(app.getString(R.string.restore_finished_error))
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -306,8 +310,9 @@ internal class RestoreViewModel(
|
||||||
val restorableBackups = restoreSets.mapNotNull { set ->
|
val restorableBackups = restoreSets.mapNotNull { set ->
|
||||||
getRestorableBackup(set, backupMetadata[set.token])
|
getRestorableBackup(set, backupMetadata[set.token])
|
||||||
}
|
}
|
||||||
if (restorableBackups.isEmpty()) RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
if (restorableBackups.isEmpty()) {
|
||||||
else RestoreSetResult(restorableBackups)
|
RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
||||||
|
} else RestoreSetResult(restorableBackups)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continuation.resume(result)
|
continuation.resume(result)
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.stevesoltys.seedvault.settings
|
||||||
|
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.BACKUP_SERVICE
|
import android.content.Context.BACKUP_SERVICE // ktlint-disable no-unused-imports
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.hardware.usb.UsbDevice
|
import android.hardware.usb.UsbDevice
|
||||||
|
@ -11,7 +11,7 @@ import android.hardware.usb.UsbManager.ACTION_USB_DEVICE_DETACHED
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE
|
import android.provider.Settings.Secure.BACKUP_AUTO_RESTORE // ktlint-disable no-unused-imports
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
|
|
@ -2,10 +2,10 @@ package com.stevesoltys.seedvault.transport
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP
|
import android.app.backup.BackupManager.FLAG_NON_INCREMENTAL_BACKUP // ktlint-disable no-unused-imports
|
||||||
import android.app.backup.IBackupManager
|
import android.app.backup.IBackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.BACKUP_SERVICE
|
import android.content.Context.BACKUP_SERVICE // ktlint-disable no-unused-imports
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.RemoteException
|
import android.os.RemoteException
|
||||||
|
|
|
@ -79,8 +79,8 @@ class ApkBackup(
|
||||||
// do not backup if we have the version already and signatures did not change
|
// do not backup if we have the version already and signatures did not change
|
||||||
if (version <= backedUpVersion && !signaturesChanged(packageMetadata, signatures)) {
|
if (version <= backedUpVersion && !signaturesChanged(packageMetadata, signatures)) {
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG, "Package $packageName with version $version" +
|
||||||
"Package $packageName with version $version already has a backup ($backedUpVersion)" +
|
" already has a backup ($backedUpVersion)" +
|
||||||
" with the same signature. Not backing it up."
|
" with the same signature. Not backing it up."
|
||||||
)
|
)
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -327,12 +327,16 @@ internal class BackupCoordinator(
|
||||||
*/
|
*/
|
||||||
suspend fun finishBackup(): Int = when {
|
suspend fun finishBackup(): Int = when {
|
||||||
kv.hasState() -> {
|
kv.hasState() -> {
|
||||||
check(!full.hasState()) { "K/V backup has state, but full backup has dangling state as well" }
|
check(!full.hasState()) {
|
||||||
|
"K/V backup has state, but full backup has dangling state as well"
|
||||||
|
}
|
||||||
onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state
|
onPackageBackedUp(kv.getCurrentPackage()!!) // not-null because we have state
|
||||||
kv.finishBackup()
|
kv.finishBackup()
|
||||||
}
|
}
|
||||||
full.hasState() -> {
|
full.hasState() -> {
|
||||||
check(!kv.hasState()) { "Full backup has state, but K/V backup has dangling state as well" }
|
check(!kv.hasState()) {
|
||||||
|
"Full backup has state, but K/V backup has dangling state as well"
|
||||||
|
}
|
||||||
onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state
|
onPackageBackedUp(full.getCurrentPackage()!!) // not-null because we have state
|
||||||
full.finishBackup()
|
full.finishBackup()
|
||||||
}
|
}
|
||||||
|
@ -352,15 +356,17 @@ internal class BackupCoordinator(
|
||||||
val packageName = packageInfo.packageName
|
val packageName = packageInfo.packageName
|
||||||
try {
|
try {
|
||||||
nm.onOptOutAppBackup(packageName, i + 1, notAllowedPackages.size)
|
nm.onOptOutAppBackup(packageName, i + 1, notAllowedPackages.size)
|
||||||
val packageState = if (packageInfo.isStopped()) WAS_STOPPED else NOT_ALLOWED
|
val packageState =
|
||||||
|
if (packageInfo.isStopped()) WAS_STOPPED else NOT_ALLOWED
|
||||||
val wasBackedUp = backUpApk(packageInfo, packageState)
|
val wasBackedUp = backUpApk(packageInfo, packageState)
|
||||||
if (!wasBackedUp) {
|
if (!wasBackedUp) {
|
||||||
val packageMetadata = metadataManager.getPackageMetadata(packageName)
|
val packageMetadata =
|
||||||
|
metadataManager.getPackageMetadata(packageName)
|
||||||
val oldPackageState = packageMetadata?.state
|
val oldPackageState = packageMetadata?.state
|
||||||
if (oldPackageState != null && oldPackageState != packageState) {
|
if (oldPackageState != null && oldPackageState != packageState) {
|
||||||
Log.e(
|
Log.e(
|
||||||
TAG,
|
TAG, "Package $packageName was in $oldPackageState" +
|
||||||
"Package $packageName was in $oldPackageState, update to $packageState"
|
", update to $packageState"
|
||||||
)
|
)
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onPackageBackupError(packageInfo, packageState, it)
|
metadataManager.onPackageBackupError(packageInfo, packageState, it)
|
||||||
|
|
|
@ -62,7 +62,7 @@ internal class ApkInstaller(private val context: Context) {
|
||||||
}
|
}
|
||||||
// Don't set more sessionParams intentionally here.
|
// Don't set more sessionParams intentionally here.
|
||||||
// We saw strange permission issues when doing setInstallReason() or setting installFlags.
|
// We saw strange permission issues when doing setInstallReason() or setting installFlags.
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
||||||
val session = installer.openSession(installer.createSession(sessionParams))
|
val session = installer.openSession(installer.createSession(sessionParams))
|
||||||
val sizeBytes = cachedApk.length()
|
val sizeBytes = cachedApk.length()
|
||||||
session.use { s ->
|
session.use { s ->
|
||||||
|
@ -96,7 +96,9 @@ internal class ApkInstaller(private val context: Context) {
|
||||||
val success = i.getIntExtra(EXTRA_STATUS, -1) == STATUS_SUCCESS
|
val success = i.getIntExtra(EXTRA_STATUS, -1) == STATUS_SUCCESS
|
||||||
val statusMsg = i.getStringExtra(EXTRA_STATUS_MESSAGE)!!
|
val statusMsg = i.getStringExtra(EXTRA_STATUS_MESSAGE)!!
|
||||||
|
|
||||||
check(packageName == expectedPackageName) { "Expected $expectedPackageName, but got $packageName." }
|
check(packageName == expectedPackageName) {
|
||||||
|
"Expected $expectedPackageName, but got $packageName."
|
||||||
|
}
|
||||||
Log.d(TAG, "Received result for $packageName: success=$success $statusMsg")
|
Log.d(TAG, "Received result for $packageName: success=$success $statusMsg")
|
||||||
|
|
||||||
// delete cached APK file
|
// delete cached APK file
|
||||||
|
|
|
@ -95,9 +95,9 @@ internal class ApkRestore(
|
||||||
|
|
||||||
// check APK's SHA-256 hash
|
// check APK's SHA-256 hash
|
||||||
val sha256 = messageDigest.digest().encodeBase64()
|
val sha256 = messageDigest.digest().encodeBase64()
|
||||||
if (metadata.sha256 != sha256) {
|
if (metadata.sha256 != sha256) throw SecurityException(
|
||||||
throw SecurityException("Package $packageName has sha256 '$sha256', but '${metadata.sha256}' expected.")
|
"Package $packageName has sha256 '$sha256', but '${metadata.sha256}' expected."
|
||||||
}
|
)
|
||||||
|
|
||||||
// parse APK (GET_SIGNATURES is needed even though deprecated)
|
// parse APK (GET_SIGNATURES is needed even though deprecated)
|
||||||
@Suppress("DEPRECATION") val flags = GET_SIGNING_CERTIFICATES or GET_SIGNATURES
|
@Suppress("DEPRECATION") val flags = GET_SIGNING_CERTIFICATES or GET_SIGNATURES
|
||||||
|
@ -105,9 +105,9 @@ internal class ApkRestore(
|
||||||
?: throw IOException("getPackageArchiveInfo returned null")
|
?: throw IOException("getPackageArchiveInfo returned null")
|
||||||
|
|
||||||
// check APK package name
|
// check APK package name
|
||||||
if (packageName != packageInfo.packageName) {
|
if (packageName != packageInfo.packageName) throw SecurityException(
|
||||||
throw SecurityException("Package $packageName expected, but ${packageInfo.packageName} found.")
|
"Package $packageName expected, but ${packageInfo.packageName} found."
|
||||||
}
|
)
|
||||||
|
|
||||||
// check APK version code
|
// check APK version code
|
||||||
if (metadata.version != packageInfo.longVersionCode) {
|
if (metadata.version != packageInfo.longVersionCode) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.stevesoltys.seedvault.ui.storage
|
package com.stevesoltys.seedvault.ui.storage
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
|
|
@ -277,9 +277,15 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
|
|
||||||
private fun getIcon(context: Context, authority: String, rootId: String, icon: Int): Drawable? {
|
private fun getIcon(context: Context, authority: String, rootId: String, icon: Int): Drawable? {
|
||||||
return getPackageIcon(context, authority, icon) ?: when {
|
return getPackageIcon(context, authority, icon) ?: when {
|
||||||
authority == AUTHORITY_STORAGE && rootId == ROOT_ID_DEVICE -> context.getDrawable(R.drawable.ic_phone_android)
|
authority == AUTHORITY_STORAGE && rootId == ROOT_ID_DEVICE -> {
|
||||||
authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> context.getDrawable(R.drawable.ic_usb)
|
context.getDrawable(R.drawable.ic_phone_android)
|
||||||
authority == AUTHORITY_NEXTCLOUD -> context.getDrawable(R.drawable.nextcloud)
|
}
|
||||||
|
authority == AUTHORITY_STORAGE && rootId != ROOT_ID_HOME -> {
|
||||||
|
context.getDrawable(R.drawable.ic_usb)
|
||||||
|
}
|
||||||
|
authority == AUTHORITY_NEXTCLOUD -> {
|
||||||
|
context.getDrawable(R.drawable.nextcloud)
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,4 @@ class KeyManagerTestImpl : KeyManager {
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2090,31 +2090,36 @@ class WordListTest {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `12 words generate expected seed`() {
|
fun `12 words generate expected seed`() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"64AA8C388EC0F3A13C7E51653BC766E30668D30952AB34381C4B174BF3278774B4EE43D0BA08BCBCE0D0B806DEB7AA364A83525C34847078B2A8002A3E116066",
|
"64AA8C388EC0F3A13C7E51653BC766E30668D30952AB34381C4B174BF3278774" +
|
||||||
|
"B4EE43D0BA08BCBCE0D0B806DEB7AA364A83525C34847078B2A8002A3E116066",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
||||||
"write wrong yard year yellow you young youth zebra zero zone zoo", ""
|
"write wrong yard year yellow you young youth zebra zero zone zoo", ""
|
||||||
).toHexString("")
|
).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"E911FAA42F389AA9F6D5A40B2ECB876B06D6D1FFBD5885C54720398EB11918CAB8F7BAD70FD5BE39BEB4EB065610700D1CFF1D4BFAA26F998357E15E79002779",
|
"E911FAA42F389AA9F6D5A40B2ECB876B06D6D1FFBD5885C54720398EB11918CA" +
|
||||||
|
"B8F7BAD70FD5BE39BEB4EB065610700D1CFF1D4BFAA26F998357E15E79002779",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
||||||
"matrix lava they brand negative spray floor gym purity picture ritual disorder", ""
|
"matrix lava they brand negative spray floor gym purity picture ritual disorder", ""
|
||||||
).toHexString("")
|
).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"DDB26091680CF30D0DC615546E4612327DB287B6B2B8B8947A3E12580315D38C3BF7DD0EB4E9E50B10A41925332E0C8ED43C80DBA29281EF331A1DFA858BF1C9",
|
"DDB26091680CF30D0DC615546E4612327DB287B6B2B8B8947A3E12580315D38C" +
|
||||||
|
"3BF7DD0EB4E9E50B10A41925332E0C8ED43C80DBA29281EF331A1DFA858BF1C9",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
||||||
"middle rack south alert ribbon tube hope involve defy oxygen gloom rabbit", ""
|
"middle rack south alert ribbon tube hope involve defy oxygen gloom rabbit", ""
|
||||||
).toHexString("")
|
).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"4815B580D0DCDA08334C92B3CB9A8436CD581C55841FB2794FB1E3D6E389F447C8C6520B2FE567720950F5B39BE7EC42C0BC98D3C63F8FEF642B5BD3EE4CDD7B",
|
"4815B580D0DCDA08334C92B3CB9A8436CD581C55841FB2794FB1E3D6E389F447" +
|
||||||
|
"C8C6520B2FE567720950F5B39BE7EC42C0BC98D3C63F8FEF642B5BD3EE4CDD7B",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
||||||
"interest mask trial hold foot segment fade page monitor apple garden shuffle", ""
|
"interest mask trial hold foot segment fade page monitor apple garden shuffle", ""
|
||||||
).toHexString("")
|
).toHexString("")
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"FF462543D8FB9DAE6C17FA7BA047238664207FCC797D6688E10DD1B3CFD183D4928AD088E8287B69BABCAEB0F87A2DFF2ADD49A7FDB7EB2554D7344F09C41A76",
|
"FF462543D8FB9DAE6C17FA7BA047238664207FCC797D6688E10DD1B3CFD183D4" +
|
||||||
|
"928AD088E8287B69BABCAEB0F87A2DFF2ADD49A7FDB7EB2554D7344F09C41A76",
|
||||||
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
SeedCalculator(JavaxPBKDF2WithHmacSHA512.INSTANCE).calculateSeed(
|
||||||
"palace glory gospel garment obscure person edge total hunt fix setup uphold\n", ""
|
"palace glory gospel garment obscure person edge total hunt fix setup uphold\n", ""
|
||||||
).toHexString("")
|
).toHexString("")
|
||||||
|
|
|
@ -160,7 +160,9 @@ class MetadataReaderTest {
|
||||||
assertNull(packageMetadata.signatures)
|
assertNull(packageMetadata.signatures)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMetadata(packageMetadata: PackageMetadataMap = PackageMetadataMap()): BackupMetadata {
|
private fun getMetadata(
|
||||||
|
packageMetadata: PackageMetadataMap = PackageMetadataMap()
|
||||||
|
): BackupMetadata {
|
||||||
return BackupMetadata(
|
return BackupMetadata(
|
||||||
version = 1.toByte(),
|
version = 1.toByte(),
|
||||||
token = Random.nextLong(),
|
token = Random.nextLong(),
|
||||||
|
|
|
@ -110,7 +110,9 @@ internal class MetadataWriterDecoderTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMetadata(packageMetadata: HashMap<String, PackageMetadata> = HashMap()): BackupMetadata {
|
private fun getMetadata(
|
||||||
|
packageMetadata: HashMap<String, PackageMetadata> = HashMap()
|
||||||
|
): BackupMetadata {
|
||||||
return BackupMetadata(
|
return BackupMetadata(
|
||||||
version = Random.nextBytes(1)[0],
|
version = Random.nextBytes(1)[0],
|
||||||
token = Random.nextLong(),
|
token = Random.nextLong(),
|
||||||
|
|
|
@ -32,7 +32,6 @@ import java.io.OutputStream
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class ApkBackupTest : BackupTest() {
|
internal class ApkBackupTest : BackupTest() {
|
||||||
|
|
||||||
|
@ -104,7 +103,9 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `do not accept empty signature`() = runBlocking {
|
fun `do not accept empty signature`() = runBlocking {
|
||||||
every { settingsManager.backupApks() } returns true
|
every { settingsManager.backupApks() } returns true
|
||||||
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns packageMetadata
|
every {
|
||||||
|
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||||
|
} returns packageMetadata
|
||||||
every { sigInfo.hasMultipleSigners() } returns false
|
every { sigInfo.hasMultipleSigners() } returns false
|
||||||
every { sigInfo.signingCertificateHistory } returns emptyArray()
|
every { sigInfo.signingCertificateHistory } returns emptyArray()
|
||||||
|
|
||||||
|
@ -151,7 +152,9 @@ internal class ApkBackupTest : BackupTest() {
|
||||||
|
|
||||||
private fun expectChecks(packageMetadata: PackageMetadata = this.packageMetadata) {
|
private fun expectChecks(packageMetadata: PackageMetadata = this.packageMetadata) {
|
||||||
every { settingsManager.backupApks() } returns true
|
every { settingsManager.backupApks() } returns true
|
||||||
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns packageMetadata
|
every {
|
||||||
|
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||||
|
} returns packageMetadata
|
||||||
every { PackageUtils.computeSha256DigestBytes(signatureBytes) } returns signatureHash
|
every { PackageUtils.computeSha256DigestBytes(signatureBytes) } returns signatureHash
|
||||||
every { sigInfo.hasMultipleSigners() } returns false
|
every { sigInfo.hasMultipleSigners() } returns false
|
||||||
every { sigInfo.signingCertificateHistory } returns sigs
|
every { sigInfo.signingCertificateHistory } returns sigs
|
||||||
|
|
|
@ -246,7 +246,9 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
coEvery { full.performFullBackup(packageInfo, fileDescriptor, 0) } returns TRANSPORT_OK
|
||||||
expectApkBackupAndMetadataWrite()
|
expectApkBackupAndMetadataWrite()
|
||||||
every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
every { full.getQuota() } returns DEFAULT_QUOTA_FULL_BACKUP
|
||||||
every { full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1) } returns TRANSPORT_QUOTA_EXCEEDED
|
every {
|
||||||
|
full.checkFullBackupSize(DEFAULT_QUOTA_FULL_BACKUP + 1)
|
||||||
|
} returns TRANSPORT_QUOTA_EXCEEDED
|
||||||
every { full.getCurrentPackage() } returns packageInfo
|
every { full.getCurrentPackage() } returns packageInfo
|
||||||
every {
|
every {
|
||||||
metadataManager.onPackageBackupError(
|
metadataManager.onPackageBackupError(
|
||||||
|
@ -347,7 +349,9 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
|
apkBackup.backupApkIfNecessary(notAllowedPackages[0], NOT_ALLOWED, any())
|
||||||
} returns null
|
} returns null
|
||||||
// check old metadata for state changes, because we won't update it otherwise
|
// check old metadata for state changes, because we won't update it otherwise
|
||||||
every { metadataManager.getPackageMetadata(notAllowedPackages[0].packageName) } returns packageMetadata
|
every {
|
||||||
|
metadataManager.getPackageMetadata(notAllowedPackages[0].packageName)
|
||||||
|
} returns packageMetadata
|
||||||
every { packageMetadata.state } returns NOT_ALLOWED // no change
|
every { packageMetadata.state } returns NOT_ALLOWED // no change
|
||||||
|
|
||||||
// update notification for second package
|
// update notification for second package
|
||||||
|
@ -386,10 +390,15 @@ internal class BackupCoordinatorTest : BackupTest() {
|
||||||
val oldPackageMetadata: PackageMetadata = mockk()
|
val oldPackageMetadata: PackageMetadata = mockk()
|
||||||
|
|
||||||
every { packageService.notAllowedPackages } returns listOf(packageInfo)
|
every { packageService.notAllowedPackages } returns listOf(packageInfo)
|
||||||
every { notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1) } just Runs
|
every {
|
||||||
|
notificationManager.onOptOutAppBackup(packageInfo.packageName, 1, 1)
|
||||||
|
} just Runs
|
||||||
coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null
|
coEvery { apkBackup.backupApkIfNecessary(packageInfo, NOT_ALLOWED, any()) } returns null
|
||||||
every { metadataManager.getPackageMetadata(packageInfo.packageName) } returns oldPackageMetadata
|
every {
|
||||||
every { oldPackageMetadata.state } returns WAS_STOPPED // state differs now, was stopped before
|
metadataManager.getPackageMetadata(packageInfo.packageName)
|
||||||
|
} returns oldPackageMetadata
|
||||||
|
// state differs now, was stopped before
|
||||||
|
every { oldPackageMetadata.state } returns WAS_STOPPED
|
||||||
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
coEvery { plugin.getMetadataOutputStream() } returns metadataOutputStream
|
||||||
every {
|
every {
|
||||||
metadataManager.onPackageBackupError(
|
metadataManager.onPackageBackupError(
|
||||||
|
|
|
@ -215,7 +215,7 @@ internal class ApkRestoreTest : RestoreTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test system apps only get reinstalled when older system apps exist`(@TempDir tmpDir: Path) =
|
fun `test system apps only reinstalled when older system apps exist`(@TempDir tmpDir: Path) =
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val packageMetadata = this@ApkRestoreTest.packageMetadata.copy(system = true)
|
val packageMetadata = this@ApkRestoreTest.packageMetadata.copy(system = true)
|
||||||
packageMetadataMap[packageName] = packageMetadata
|
packageMetadataMap[packageName] = packageMetadata
|
||||||
|
|
|
@ -165,7 +165,7 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `startRestore() optimized auto-restore with removed storage but no backup shows no notification`() {
|
fun `startRestore() with removed storage shows no notification`() {
|
||||||
every { settingsManager.getStorage() } returns storage
|
every { settingsManager.getStorage() } returns storage
|
||||||
every { storage.isUsb } returns true
|
every { storage.isUsb } returns true
|
||||||
every { storage.getDocumentFile(context) } returns documentFile
|
every { storage.getDocumentFile(context) } returns documentFile
|
||||||
|
|
Loading…
Reference in a new issue