Auto-format code style of all files to match official style
This also adds a note to the README and the Android Studio coding style files.
This commit is contained in:
parent
55909ce305
commit
53937bda2f
72 changed files with 1776 additions and 1166 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,6 +9,7 @@ out/
|
||||||
lib/
|
lib/
|
||||||
.idea/*
|
.idea/*
|
||||||
!.idea/runConfigurations*
|
!.idea/runConfigurations*
|
||||||
|
!.idea/codeStyles*
|
||||||
*.ipr
|
*.ipr
|
||||||
*.iws
|
*.iws
|
||||||
*.iml
|
*.iml
|
||||||
|
|
139
.idea/codeStyles/Project.xml
Normal file
139
.idea/codeStyles/Project.xml
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="PACKAGES_IMPORT_LAYOUT">
|
||||||
|
<value>
|
||||||
|
<package name="" alias="false" withSubpackages="true" />
|
||||||
|
<package name="java" alias="false" withSubpackages="true" />
|
||||||
|
<package name="javax" alias="false" withSubpackages="true" />
|
||||||
|
<package name="kotlin" alias="false" withSubpackages="true" />
|
||||||
|
<package name="" alias="true" withSubpackages="true" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||||
|
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
|
@ -32,7 +32,9 @@ It uses the same internal APIs as `adb backup` which is deprecated and thus need
|
||||||
* `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup.
|
* `android.permission.INSTALL_PACKAGES` to re-install apps when restoring from backup.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Bug reports and pull requests are welcome on GitHub at https://github.com/stevesoltys/seedvault.
|
Bug reports and pull requests are welcome on GitHub at https://github.com/stevesoltys/seedvault.
|
||||||
|
|
||||||
|
This project aims to adhere to the [official Kotlin coding style](https://developer.android.com/kotlin/style-guide).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This application is available as open source under the terms of the [Apache-2.0 License](https://opensource.org/licenses/Apache-2.0).
|
This application is available as open source under the terms of the [Apache-2.0 License](https://opensource.org/licenses/Apache-2.0).
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package com.stevesoltys.seedvault
|
package com.stevesoltys.seedvault
|
||||||
|
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.util.*
|
import java.util.Base64
|
||||||
|
|
||||||
val Utf8: Charset = Charset.forName("UTF-8")
|
val Utf8: Charset = Charset.forName("UTF-8")
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,8 @@ import java.util.concurrent.TimeUnit.HOURS
|
||||||
|
|
||||||
private val TAG = UsbIntentReceiver::class.java.simpleName
|
private val TAG = UsbIntentReceiver::class.java.simpleName
|
||||||
|
|
||||||
|
private const val HOURS_AUTO_BACKUP: Long = 24
|
||||||
|
|
||||||
class UsbIntentReceiver : UsbMonitor() {
|
class UsbIntentReceiver : UsbMonitor() {
|
||||||
|
|
||||||
// using KoinComponent would crash robolectric tests :(
|
// using KoinComponent would crash robolectric tests :(
|
||||||
|
@ -37,7 +39,8 @@ class UsbIntentReceiver : UsbMonitor() {
|
||||||
val attachedFlashDrive = FlashDrive.from(device)
|
val attachedFlashDrive = FlashDrive.from(device)
|
||||||
return if (savedFlashDrive == attachedFlashDrive) {
|
return if (savedFlashDrive == attachedFlashDrive) {
|
||||||
Log.d(TAG, "Matches stored device, checking backup time...")
|
Log.d(TAG, "Matches stored device, checking backup time...")
|
||||||
if (System.currentTimeMillis() - metadataManager.getLastBackupTime() >= HOURS.toMillis(24)) {
|
val backupMillis = System.currentTimeMillis() - metadataManager.getLastBackupTime()
|
||||||
|
if (backupMillis >= HOURS.toMillis(HOURS_AUTO_BACKUP)) {
|
||||||
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
|
Log.d(TAG, "Last backup older than 24 hours, requesting a backup...")
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
@ -101,6 +104,7 @@ internal fun UsbDevice.isMassStorage(): Boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun UsbInterface.isMassStorage(): Boolean {
|
private fun UsbInterface.isMassStorage(): Boolean {
|
||||||
|
@Suppress("MagicNumber")
|
||||||
return interfaceClass == 8 && interfaceProtocol == 80 && interfaceSubclass == 6
|
return interfaceClass == 8 && interfaceProtocol == 80 && interfaceSubclass == 6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,8 +75,12 @@ interface Crypto {
|
||||||
* @return The read [VersionHeader] present in the beginning of the given [InputStream].
|
* @return The read [VersionHeader] present in the beginning of the given [InputStream].
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
fun decryptHeader(inputStream: InputStream, expectedVersion: Byte, expectedPackageName: String,
|
fun decryptHeader(
|
||||||
expectedKey: String? = null): VersionHeader
|
inputStream: InputStream,
|
||||||
|
expectedVersion: Byte,
|
||||||
|
expectedPackageName: String,
|
||||||
|
expectedKey: String? = null
|
||||||
|
): VersionHeader
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads and decrypts a segment from the given [InputStream].
|
* Reads and decrypts a segment from the given [InputStream].
|
||||||
|
@ -94,9 +98,10 @@ interface Crypto {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class CryptoImpl(
|
internal class CryptoImpl(
|
||||||
private val cipherFactory: CipherFactory,
|
private val cipherFactory: CipherFactory,
|
||||||
private val headerWriter: HeaderWriter,
|
private val headerWriter: HeaderWriter,
|
||||||
private val headerReader: HeaderReader) : Crypto {
|
private val headerReader: HeaderReader
|
||||||
|
) : Crypto {
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
override fun encryptHeader(outputStream: OutputStream, versionHeader: VersionHeader) {
|
override fun encryptHeader(outputStream: OutputStream, versionHeader: VersionHeader) {
|
||||||
|
@ -136,16 +141,26 @@ internal class CryptoImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
override fun decryptHeader(inputStream: InputStream, expectedVersion: Byte,
|
override fun decryptHeader(
|
||||||
expectedPackageName: String, expectedKey: String?): VersionHeader {
|
inputStream: InputStream,
|
||||||
|
expectedVersion: Byte,
|
||||||
|
expectedPackageName: String,
|
||||||
|
expectedKey: String?
|
||||||
|
): VersionHeader {
|
||||||
val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
|
val decrypted = decryptSegment(inputStream, MAX_VERSION_HEADER_SIZE)
|
||||||
val header = headerReader.getVersionHeader(decrypted)
|
val header = headerReader.getVersionHeader(decrypted)
|
||||||
|
|
||||||
if (header.version != expectedVersion) {
|
if (header.version != expectedVersion) {
|
||||||
throw SecurityException("Invalid version '${header.version.toInt()}' in header, expected '${expectedVersion.toInt()}'.")
|
throw SecurityException(
|
||||||
|
"Invalid version '${header.version.toInt()}' in header, " +
|
||||||
|
"expected '${expectedVersion.toInt()}'."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (header.packageName != expectedPackageName) {
|
if (header.packageName != expectedPackageName) {
|
||||||
throw SecurityException("Invalid package name '${header.packageName}' in header, expected '$expectedPackageName'.")
|
throw SecurityException(
|
||||||
|
"Invalid package name '${header.packageName}' in header, " +
|
||||||
|
"expected '$expectedPackageName'."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (header.key != expectedKey) {
|
if (header.key != expectedKey) {
|
||||||
throw SecurityException("Invalid key '${header.key}' in header, expected '$expectedKey'.")
|
throw SecurityException("Invalid key '${header.key}' in header, expected '$expectedKey'.")
|
||||||
|
|
|
@ -54,7 +54,7 @@ internal class KeyManagerImpl : KeyManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasBackupKey() = keyStore.containsAlias(KEY_ALIAS) &&
|
override fun hasBackupKey() = keyStore.containsAlias(KEY_ALIAS) &&
|
||||||
keyStore.entryInstanceOf(KEY_ALIAS, SecretKeyEntry::class.java)
|
keyStore.entryInstanceOf(KEY_ALIAS, SecretKeyEntry::class.java)
|
||||||
|
|
||||||
override fun getBackupKey(): SecretKey {
|
override fun getBackupKey(): SecretKey {
|
||||||
val ksEntry = keyStore.getEntry(KEY_ALIAS, null) as SecretKeyEntry
|
val ksEntry = keyStore.getEntry(KEY_ALIAS, null) as SecretKeyEntry
|
||||||
|
@ -63,9 +63,9 @@ internal class KeyManagerImpl : KeyManager {
|
||||||
|
|
||||||
private fun getKeyProtection(): KeyProtection {
|
private fun getKeyProtection(): KeyProtection {
|
||||||
val builder = KeyProtection.Builder(PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
|
val builder = KeyProtection.Builder(PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
|
||||||
.setBlockModes(BLOCK_MODE_GCM)
|
.setBlockModes(BLOCK_MODE_GCM)
|
||||||
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
|
||||||
.setRandomizedEncryptionRequired(true)
|
.setRandomizedEncryptionRequired(true)
|
||||||
// unlocking is required only for decryption, so when restoring from backup
|
// unlocking is required only for decryption, so when restoring from backup
|
||||||
builder.setUnlockedDeviceRequired(true)
|
builder.setUnlockedDeviceRequired(true)
|
||||||
return builder.build()
|
return builder.build()
|
||||||
|
|
|
@ -5,16 +5,17 @@ import com.stevesoltys.seedvault.crypto.GCM_AUTHENTICATION_TAG_LENGTH
|
||||||
internal const val VERSION: Byte = 0
|
internal const val VERSION: Byte = 0
|
||||||
internal const val MAX_PACKAGE_LENGTH_SIZE = 255
|
internal const val MAX_PACKAGE_LENGTH_SIZE = 255
|
||||||
internal const val MAX_KEY_LENGTH_SIZE = MAX_PACKAGE_LENGTH_SIZE
|
internal const val MAX_KEY_LENGTH_SIZE = MAX_PACKAGE_LENGTH_SIZE
|
||||||
internal const val MAX_VERSION_HEADER_SIZE = 1 + Short.SIZE_BYTES * 2 + MAX_PACKAGE_LENGTH_SIZE + MAX_KEY_LENGTH_SIZE
|
internal const val MAX_VERSION_HEADER_SIZE =
|
||||||
|
1 + Short.SIZE_BYTES * 2 + MAX_PACKAGE_LENGTH_SIZE + MAX_KEY_LENGTH_SIZE
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After the first version byte of each backup stream
|
* After the first version byte of each backup stream
|
||||||
* must follow followed this header encrypted with authentication.
|
* must follow followed this header encrypted with authentication.
|
||||||
*/
|
*/
|
||||||
data class VersionHeader(
|
data class VersionHeader(
|
||||||
internal val version: Byte = VERSION, // 1 byte
|
internal val version: Byte = VERSION, // 1 byte
|
||||||
internal val packageName: String, // ?? bytes (max 255)
|
internal val packageName: String, // ?? bytes (max 255)
|
||||||
internal val key: String? = null // ?? bytes
|
internal val key: String? = null // ?? bytes
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
check(packageName.length <= MAX_PACKAGE_LENGTH_SIZE) {
|
check(packageName.length <= MAX_PACKAGE_LENGTH_SIZE) {
|
||||||
|
@ -26,10 +27,10 @@ data class VersionHeader(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal const val SEGMENT_LENGTH_SIZE: Int = Short.SIZE_BYTES
|
internal const val SEGMENT_LENGTH_SIZE: Int = Short.SIZE_BYTES
|
||||||
internal const val MAX_SEGMENT_LENGTH: Int = Short.MAX_VALUE.toInt()
|
internal const val MAX_SEGMENT_LENGTH: Int = Short.MAX_VALUE.toInt()
|
||||||
internal const val MAX_SEGMENT_CLEARTEXT_LENGTH: Int = MAX_SEGMENT_LENGTH - GCM_AUTHENTICATION_TAG_LENGTH / 8
|
internal const val MAX_SEGMENT_CLEARTEXT_LENGTH: Int =
|
||||||
|
MAX_SEGMENT_LENGTH - GCM_AUTHENTICATION_TAG_LENGTH / 8
|
||||||
internal const val IV_SIZE: Int = 12
|
internal const val IV_SIZE: Int = 12
|
||||||
internal const val SEGMENT_HEADER_SIZE = SEGMENT_LENGTH_SIZE + IV_SIZE
|
internal const val SEGMENT_HEADER_SIZE = SEGMENT_LENGTH_SIZE + IV_SIZE
|
||||||
|
|
||||||
|
@ -37,8 +38,8 @@ internal const val SEGMENT_HEADER_SIZE = SEGMENT_LENGTH_SIZE + IV_SIZE
|
||||||
* Each data segment must start with this header
|
* Each data segment must start with this header
|
||||||
*/
|
*/
|
||||||
class SegmentHeader(
|
class SegmentHeader(
|
||||||
internal val segmentLength: Short, // 2 bytes
|
internal val segmentLength: Short, // 2 bytes
|
||||||
internal val nonce: ByteArray // 12 bytes
|
internal val nonce: ByteArray // 12 bytes
|
||||||
) {
|
) {
|
||||||
init {
|
init {
|
||||||
check(nonce.size == IV_SIZE) {
|
check(nonce.size == IV_SIZE) {
|
||||||
|
|
|
@ -36,16 +36,16 @@ internal class HeaderReaderImpl : HeaderReader {
|
||||||
if (packageLength > MAX_PACKAGE_LENGTH_SIZE) throw SecurityException("Too large package length: $packageLength")
|
if (packageLength > MAX_PACKAGE_LENGTH_SIZE) throw SecurityException("Too large package length: $packageLength")
|
||||||
if (packageLength > buffer.remaining()) throw SecurityException("Not enough bytes for package name")
|
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) }
|
||||||
.toString(Utf8)
|
.toString(Utf8)
|
||||||
|
|
||||||
if (buffer.remaining() != 0) throw SecurityException("Found extra bytes in header")
|
if (buffer.remaining() != 0) throw SecurityException("Found extra bytes in header")
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,8 @@ internal class HeaderWriterImpl : HeaderWriter {
|
||||||
|
|
||||||
override fun writeSegmentHeader(outputStream: OutputStream, header: SegmentHeader) {
|
override fun writeSegmentHeader(outputStream: OutputStream, header: SegmentHeader) {
|
||||||
val buffer = ByteBuffer.allocate(SEGMENT_HEADER_SIZE)
|
val buffer = ByteBuffer.allocate(SEGMENT_HEADER_SIZE)
|
||||||
.putShort(header.segmentLength)
|
.putShort(header.segmentLength)
|
||||||
.put(header.nonce)
|
.put(header.nonce)
|
||||||
outputStream.write(buffer.array())
|
outputStream.write(buffer.array())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,13 @@ import java.io.InputStream
|
||||||
typealias PackageMetadataMap = HashMap<String, PackageMetadata>
|
typealias PackageMetadataMap = HashMap<String, PackageMetadata>
|
||||||
|
|
||||||
data class BackupMetadata(
|
data class BackupMetadata(
|
||||||
internal val version: Byte = VERSION,
|
internal val version: Byte = VERSION,
|
||||||
internal val token: Long,
|
internal val token: Long,
|
||||||
internal var time: Long = 0L,
|
internal var time: Long = 0L,
|
||||||
internal val androidVersion: Int = Build.VERSION.SDK_INT,
|
internal val androidVersion: Int = Build.VERSION.SDK_INT,
|
||||||
internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
|
internal val androidIncremental: String = Build.VERSION.INCREMENTAL,
|
||||||
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
|
internal val deviceName: String = "${Build.MANUFACTURER} ${Build.MODEL}",
|
||||||
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap()
|
internal val packageMetadataMap: PackageMetadataMap = PackageMetadataMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
internal const val JSON_METADATA = "@meta@"
|
internal const val JSON_METADATA = "@meta@"
|
||||||
|
@ -32,23 +32,28 @@ enum class PackageState {
|
||||||
* This is the expected state of all user-installed packages.
|
* This is the expected state of all user-installed packages.
|
||||||
*/
|
*/
|
||||||
APK_AND_DATA,
|
APK_AND_DATA,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package data could not get backed up, because the app exceeded the allowed quota.
|
* Package data could not get backed up, because the app exceeded the allowed quota.
|
||||||
*/
|
*/
|
||||||
QUOTA_EXCEEDED,
|
QUOTA_EXCEEDED,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package data could not get backed up, because the app reported no data to back up.
|
* Package data could not get backed up, because the app reported no data to back up.
|
||||||
*/
|
*/
|
||||||
NO_DATA,
|
NO_DATA,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package data could not get backed up, because the app has [FLAG_STOPPED].
|
* Package data could not get backed up, because the app has [FLAG_STOPPED].
|
||||||
*/
|
*/
|
||||||
WAS_STOPPED,
|
WAS_STOPPED,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package data could not get backed up, because it was not allowed.
|
* Package data could not get backed up, because it was not allowed.
|
||||||
* Most often, this is a manifest opt-out, but it could also be a disabled or system-user app.
|
* Most often, this is a manifest opt-out, but it could also be a disabled or system-user app.
|
||||||
*/
|
*/
|
||||||
NOT_ALLOWED,
|
NOT_ALLOWED,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Package data could not get backed up, because an error occurred during backup.
|
* Package data could not get backed up, because an error occurred during backup.
|
||||||
*/
|
*/
|
||||||
|
@ -56,17 +61,17 @@ enum class PackageState {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PackageMetadata(
|
data class PackageMetadata(
|
||||||
/**
|
/**
|
||||||
* The timestamp in milliseconds of the last app data backup.
|
* The timestamp in milliseconds of the last app data backup.
|
||||||
* It is 0 if there never was a data backup.
|
* It is 0 if there never was a data backup.
|
||||||
*/
|
*/
|
||||||
internal var time: Long = 0L,
|
internal var time: Long = 0L,
|
||||||
internal var state: PackageState = UNKNOWN_ERROR,
|
internal var state: PackageState = UNKNOWN_ERROR,
|
||||||
internal val system: Boolean = false,
|
internal val system: Boolean = false,
|
||||||
internal val version: Long? = null,
|
internal val version: Long? = null,
|
||||||
internal val installer: String? = null,
|
internal val installer: String? = null,
|
||||||
internal val sha256: String? = null,
|
internal val sha256: String? = null,
|
||||||
internal val signatures: List<String>? = null
|
internal val signatures: List<String>? = null
|
||||||
) {
|
) {
|
||||||
fun hasApk(): Boolean {
|
fun hasApk(): Boolean {
|
||||||
return version != null && sha256 != null && signatures != null
|
return version != null && sha256 != null && signatures != null
|
||||||
|
@ -84,11 +89,13 @@ internal const val JSON_PACKAGE_SIGNATURES = "signatures"
|
||||||
internal class DecryptionFailedException(cause: Throwable) : Exception(cause)
|
internal class DecryptionFailedException(cause: Throwable) : Exception(cause)
|
||||||
|
|
||||||
class EncryptedBackupMetadata private constructor(
|
class EncryptedBackupMetadata private constructor(
|
||||||
val token: Long,
|
val token: Long,
|
||||||
val inputStream: InputStream?,
|
val inputStream: InputStream?,
|
||||||
val error: Boolean) {
|
val error: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
|
constructor(token: Long, inputStream: InputStream) : this(token, inputStream, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that there was an error retrieving the encrypted backup metadata.
|
* Indicates that there was an error retrieving the encrypted backup metadata.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -182,7 +182,10 @@ class MetadataManager(
|
||||||
* If the token is 0L, it is not yet initialized and must not be used for anything.
|
* If the token is 0L, it is not yet initialized and must not be used for anything.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@Deprecated("Responsibility for current token moved to SettingsManager", ReplaceWith("settingsManager.getToken()"))
|
@Deprecated(
|
||||||
|
"Responsibility for current token moved to SettingsManager",
|
||||||
|
ReplaceWith("settingsManager.getToken()")
|
||||||
|
)
|
||||||
fun getBackupToken(): Long = metadata.token
|
fun getBackupToken(): Long = metadata.token
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -207,9 +210,9 @@ class MetadataManager(
|
||||||
// because we have no way to also include upgraded system apps
|
// because we have no way to also include upgraded system apps
|
||||||
return metadata.packageMetadataMap.filter { (_, packageMetadata) ->
|
return metadata.packageMetadataMap.filter { (_, packageMetadata) ->
|
||||||
!packageMetadata.system && ( // ignore system apps
|
!packageMetadata.system && ( // ignore system apps
|
||||||
packageMetadata.state == APK_AND_DATA || // either full success
|
packageMetadata.state == APK_AND_DATA || // either full success
|
||||||
packageMetadata.state == NO_DATA // or apps that simply had no data
|
packageMetadata.state == NO_DATA // or apps that simply had no data
|
||||||
)
|
)
|
||||||
}.count()
|
}.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,31 @@ import javax.crypto.AEADBadTagException
|
||||||
|
|
||||||
interface MetadataReader {
|
interface MetadataReader {
|
||||||
|
|
||||||
@Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class)
|
@Throws(
|
||||||
|
SecurityException::class,
|
||||||
|
DecryptionFailedException::class,
|
||||||
|
UnsupportedVersionException::class,
|
||||||
|
IOException::class
|
||||||
|
)
|
||||||
fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata
|
fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata
|
||||||
|
|
||||||
@Throws(SecurityException::class)
|
@Throws(SecurityException::class)
|
||||||
fun decode(bytes: ByteArray, expectedVersion: Byte? = null, expectedToken: Long? = null): BackupMetadata
|
fun decode(
|
||||||
|
bytes: ByteArray,
|
||||||
|
expectedVersion: Byte? = null,
|
||||||
|
expectedToken: Long? = null
|
||||||
|
): BackupMetadata
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
|
|
||||||
@Throws(SecurityException::class, DecryptionFailedException::class, UnsupportedVersionException::class, IOException::class)
|
@Throws(
|
||||||
|
SecurityException::class,
|
||||||
|
DecryptionFailedException::class,
|
||||||
|
UnsupportedVersionException::class,
|
||||||
|
IOException::class
|
||||||
|
)
|
||||||
override fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
override fun readMetadata(inputStream: InputStream, expectedToken: Long): BackupMetadata {
|
||||||
val version = inputStream.read().toByte()
|
val version = inputStream.read().toByte()
|
||||||
if (version < 0) throw IOException()
|
if (version < 0) throw IOException()
|
||||||
|
@ -42,7 +56,11 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(SecurityException::class)
|
@Throws(SecurityException::class)
|
||||||
override fun decode(bytes: ByteArray, expectedVersion: Byte?, expectedToken: Long?): BackupMetadata {
|
override fun decode(
|
||||||
|
bytes: ByteArray,
|
||||||
|
expectedVersion: Byte?,
|
||||||
|
expectedToken: Long?
|
||||||
|
): BackupMetadata {
|
||||||
// NOTE: We don't do extensive validation of the parsed input here,
|
// NOTE: We don't do extensive validation of the parsed input here,
|
||||||
// because it was encrypted with authentication, so we should be able to trust it.
|
// because it was encrypted with authentication, so we should be able to trust it.
|
||||||
//
|
//
|
||||||
|
@ -54,7 +72,10 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
val meta = json.getJSONObject(JSON_METADATA)
|
val meta = json.getJSONObject(JSON_METADATA)
|
||||||
val version = meta.getInt(JSON_METADATA_VERSION).toByte()
|
val version = meta.getInt(JSON_METADATA_VERSION).toByte()
|
||||||
if (expectedVersion != null && version != expectedVersion) {
|
if (expectedVersion != null && version != expectedVersion) {
|
||||||
throw SecurityException("Invalid version '${version.toInt()}' in metadata, expected '${expectedVersion.toInt()}'.")
|
throw SecurityException(
|
||||||
|
"Invalid version '${version.toInt()}' in metadata," +
|
||||||
|
"expected '${expectedVersion.toInt()}'."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val token = meta.getLong(JSON_METADATA_TOKEN)
|
val token = meta.getLong(JSON_METADATA_TOKEN)
|
||||||
if (expectedToken != null && token != expectedToken) {
|
if (expectedToken != null && token != expectedToken) {
|
||||||
|
@ -78,30 +99,31 @@ internal class MetadataReaderImpl(private val crypto: Crypto) : MetadataReader {
|
||||||
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER)
|
val pInstaller = p.optString(JSON_PACKAGE_INSTALLER)
|
||||||
val pSha256 = p.optString(JSON_PACKAGE_SHA256)
|
val pSha256 = p.optString(JSON_PACKAGE_SHA256)
|
||||||
val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES)
|
val pSignatures = p.optJSONArray(JSON_PACKAGE_SIGNATURES)
|
||||||
val signatures = if (pSignatures == null) null else
|
val signatures = if (pSignatures == null) null else {
|
||||||
ArrayList<String>(pSignatures.length()).apply {
|
ArrayList<String>(pSignatures.length()).apply {
|
||||||
for (i in (0 until pSignatures.length())) {
|
for (i in (0 until pSignatures.length())) {
|
||||||
add(pSignatures.getString(i))
|
add(pSignatures.getString(i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
packageMetadataMap[packageName] = PackageMetadata(
|
packageMetadataMap[packageName] = PackageMetadata(
|
||||||
time = p.getLong(JSON_PACKAGE_TIME),
|
time = p.getLong(JSON_PACKAGE_TIME),
|
||||||
state = pState,
|
state = pState,
|
||||||
system = pSystem,
|
system = pSystem,
|
||||||
version = if (pVersion == 0L) null else pVersion,
|
version = if (pVersion == 0L) null else pVersion,
|
||||||
installer = if (pInstaller == "") null else pInstaller,
|
installer = if (pInstaller == "") null else pInstaller,
|
||||||
sha256 = if (pSha256 == "") null else pSha256,
|
sha256 = if (pSha256 == "") null else pSha256,
|
||||||
signatures = signatures
|
signatures = signatures
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return BackupMetadata(
|
return BackupMetadata(
|
||||||
version = version,
|
version = version,
|
||||||
token = token,
|
token = token,
|
||||||
time = meta.getLong(JSON_METADATA_TIME),
|
time = meta.getLong(JSON_METADATA_TIME),
|
||||||
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
|
androidVersion = meta.getInt(JSON_METADATA_SDK_INT),
|
||||||
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
|
androidIncremental = meta.getString(JSON_METADATA_INCREMENTAL),
|
||||||
deviceName = meta.getString(JSON_METADATA_NAME),
|
deviceName = meta.getString(JSON_METADATA_NAME),
|
||||||
packageMetadataMap = packageMetadataMap
|
packageMetadataMap = packageMetadataMap
|
||||||
)
|
)
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) {
|
||||||
throw SecurityException(e)
|
throw SecurityException(e)
|
||||||
|
|
|
@ -18,14 +18,22 @@ import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
|
|
||||||
internal class InstallProgressAdapter : Adapter<AppInstallViewHolder>() {
|
internal class InstallProgressAdapter : Adapter<AppInstallViewHolder>() {
|
||||||
|
|
||||||
private val items = SortedList<ApkRestoreResult>(ApkRestoreResult::class.java, object : SortedListAdapterCallback<ApkRestoreResult>(this) {
|
private val items = SortedList<ApkRestoreResult>(
|
||||||
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.packageName == item2.packageName
|
ApkRestoreResult::class.java,
|
||||||
override fun areContentsTheSame(oldItem: ApkRestoreResult, newItem: ApkRestoreResult) = oldItem == newItem
|
object : SortedListAdapterCallback<ApkRestoreResult>(this) {
|
||||||
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) = item1.compareTo(item2)
|
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) =
|
||||||
})
|
item1.packageName == item2.packageName
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ApkRestoreResult, newItem: ApkRestoreResult) =
|
||||||
|
oldItem == newItem
|
||||||
|
|
||||||
|
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) =
|
||||||
|
item1.compareTo(item2)
|
||||||
|
})
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppInstallViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppInstallViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.list_item_app_status, parent, false)
|
||||||
return AppInstallViewHolder(v)
|
return AppInstallViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,10 @@ import android.app.backup.RestoreSet
|
||||||
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
|
||||||
|
|
||||||
data class RestorableBackup(private val restoreSet: RestoreSet,
|
data class RestorableBackup(
|
||||||
private val backupMetadata: BackupMetadata) {
|
private val restoreSet: RestoreSet,
|
||||||
|
private val backupMetadata: BackupMetadata
|
||||||
|
) {
|
||||||
|
|
||||||
val name: String
|
val name: String
|
||||||
get() = restoreSet.name
|
get() = restoreSet.name
|
||||||
|
|
|
@ -23,6 +23,7 @@ class RestoreErrorBroadcastReceiver : BroadcastReceiver() {
|
||||||
notificationManager.onRestoreErrorSeen()
|
notificationManager.onRestoreErrorSeen()
|
||||||
|
|
||||||
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()
|
||||||
|
|
|
@ -17,7 +17,8 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
private val items = LinkedList<AppRestoreResult>()
|
private val items = LinkedList<AppRestoreResult>()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PackageViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.list_item_app_status, parent, false)
|
||||||
return PackageViewHolder(v)
|
return PackageViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +36,9 @@ internal class RestoreProgressAdapter : Adapter<PackageViewHolder>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Diff(
|
private class Diff(
|
||||||
private val oldItems: LinkedList<AppRestoreResult>,
|
private val oldItems: LinkedList<AppRestoreResult>,
|
||||||
private val newItems: LinkedList<AppRestoreResult>) : DiffUtil.Callback() {
|
private val newItems: LinkedList<AppRestoreResult>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
override fun getOldListSize() = oldItems.size
|
override fun getOldListSize() = oldItems.size
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
@ -81,6 +83,7 @@ enum class AppRestoreStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class AppRestoreResult(
|
internal data class AppRestoreResult(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val name: CharSequence,
|
val name: CharSequence,
|
||||||
val status: AppRestoreStatus)
|
val status: AppRestoreStatus
|
||||||
|
)
|
||||||
|
|
|
@ -32,8 +32,11 @@ class RestoreProgressFragment : Fragment() {
|
||||||
private lateinit var appList: RecyclerView
|
private lateinit var appList: RecyclerView
|
||||||
private lateinit var button: Button
|
private lateinit var button: Button
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
override fun onCreateView(
|
||||||
savedInstanceState: Bundle?): View? {
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_restore_progress, container, false)
|
||||||
|
|
||||||
progressBar = v.findViewById(R.id.progressBar)
|
progressBar = v.findViewById(R.id.progressBar)
|
||||||
|
|
|
@ -13,12 +13,13 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
|
import com.stevesoltys.seedvault.restore.RestoreSetAdapter.RestoreSetViewHolder
|
||||||
|
|
||||||
internal class RestoreSetAdapter(
|
internal class RestoreSetAdapter(
|
||||||
private val listener: RestorableBackupClickListener,
|
private val listener: RestorableBackupClickListener,
|
||||||
private val items: List<RestorableBackup>) : Adapter<RestoreSetViewHolder>() {
|
private val items: List<RestorableBackup>
|
||||||
|
) : Adapter<RestoreSetViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RestoreSetViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RestoreSetViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_restore_set, parent, false) as View
|
.inflate(R.layout.list_item_restore_set, parent, false) as View
|
||||||
return RestoreSetViewHolder(v)
|
return RestoreSetViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +40,8 @@ internal class RestoreSetAdapter(
|
||||||
|
|
||||||
val lastBackup = getRelativeTime(item.time)
|
val lastBackup = getRelativeTime(item.time)
|
||||||
val setup = getRelativeTime(item.token)
|
val setup = getRelativeTime(item.token)
|
||||||
subtitleView.text = v.context.getString(R.string.restore_restore_set_times, lastBackup, setup)
|
subtitleView.text =
|
||||||
|
v.context.getString(R.string.restore_restore_set_times, lastBackup, setup)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRelativeTime(time: Long): CharSequence {
|
private fun getRelativeTime(time: Long): CharSequence {
|
||||||
|
|
|
@ -19,7 +19,7 @@ import com.stevesoltys.seedvault.BackupMonitor
|
||||||
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
import com.stevesoltys.seedvault.MAGIC_PACKAGE_MANAGER
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.crypto.KeyManager
|
import com.stevesoltys.seedvault.crypto.KeyManager
|
||||||
import com.stevesoltys.seedvault.ui.notification.getAppName
|
import com.stevesoltys.seedvault.metadata.BackupMetadata
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.APK_AND_DATA
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
import com.stevesoltys.seedvault.metadata.PackageState.NOT_ALLOWED
|
||||||
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
import com.stevesoltys.seedvault.metadata.PackageState.NO_DATA
|
||||||
|
@ -44,6 +44,7 @@ import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
|
||||||
import com.stevesoltys.seedvault.ui.LiveEvent
|
import com.stevesoltys.seedvault.ui.LiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
import com.stevesoltys.seedvault.ui.MutableLiveEvent
|
||||||
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.getAppName
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
@ -52,7 +53,7 @@ import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.onCompletion
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.LinkedList
|
||||||
import kotlin.coroutines.Continuation
|
import kotlin.coroutines.Continuation
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
@ -60,13 +61,13 @@ import kotlin.coroutines.suspendCoroutine
|
||||||
private val TAG = RestoreViewModel::class.java.simpleName
|
private val TAG = RestoreViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal class RestoreViewModel(
|
internal class RestoreViewModel(
|
||||||
app: Application,
|
app: Application,
|
||||||
settingsManager: SettingsManager,
|
settingsManager: SettingsManager,
|
||||||
keyManager: KeyManager,
|
keyManager: KeyManager,
|
||||||
private val backupManager: IBackupManager,
|
private val backupManager: IBackupManager,
|
||||||
private val restoreCoordinator: RestoreCoordinator,
|
private val restoreCoordinator: RestoreCoordinator,
|
||||||
private val apkRestore: ApkRestore,
|
private val apkRestore: ApkRestore,
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||||
) : RequireProvisioningViewModel(app, settingsManager, keyManager), RestorableBackupClickListener {
|
) : RequireProvisioningViewModel(app, settingsManager, keyManager), RestorableBackupClickListener {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
override val isRestoreOperation = true
|
||||||
|
@ -83,17 +84,24 @@ internal class RestoreViewModel(
|
||||||
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
private val mChosenRestorableBackup = MutableLiveData<RestorableBackup>()
|
||||||
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
internal val chosenRestorableBackup: LiveData<RestorableBackup> get() = mChosenRestorableBackup
|
||||||
|
|
||||||
internal val installResult: LiveData<InstallResult> = switchMap(mChosenRestorableBackup) { backup ->
|
internal val installResult: LiveData<InstallResult> =
|
||||||
@Suppress("EXPERIMENTAL_API_USAGE")
|
switchMap(mChosenRestorableBackup) { backup ->
|
||||||
getInstallResult(backup)
|
@Suppress("EXPERIMENTAL_API_USAGE")
|
||||||
}
|
getInstallResult(backup)
|
||||||
|
}
|
||||||
|
|
||||||
private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
|
private val mNextButtonEnabled = MutableLiveData<Boolean>().apply { value = false }
|
||||||
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
|
internal val nextButtonEnabled: LiveData<Boolean> = mNextButtonEnabled
|
||||||
|
|
||||||
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
|
private val mRestoreProgress = MutableLiveData<LinkedList<AppRestoreResult>>().apply {
|
||||||
value = LinkedList<AppRestoreResult>().apply {
|
value = LinkedList<AppRestoreResult>().apply {
|
||||||
add(AppRestoreResult(MAGIC_PACKAGE_MANAGER, getAppName(app, MAGIC_PACKAGE_MANAGER), IN_PROGRESS))
|
add(
|
||||||
|
AppRestoreResult(
|
||||||
|
packageName = MAGIC_PACKAGE_MANAGER,
|
||||||
|
name = getAppName(app, MAGIC_PACKAGE_MANAGER),
|
||||||
|
status = IN_PROGRESS
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
internal val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
|
internal val restoreProgress: LiveData<LinkedList<AppRestoreResult>> get() = mRestoreProgress
|
||||||
|
@ -104,8 +112,8 @@ internal class RestoreViewModel(
|
||||||
@Throws(RemoteException::class)
|
@Throws(RemoteException::class)
|
||||||
private fun getOrStartSession(): IRestoreSession {
|
private fun getOrStartSession(): IRestoreSession {
|
||||||
val session = this.session
|
val session = this.session
|
||||||
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
|
?: backupManager.beginRestoreSessionForUser(UserHandle.myUserId(), null, TRANSPORT_ID)
|
||||||
?: throw RemoteException("beginRestoreSessionForUser returned null")
|
?: throw RemoteException("beginRestoreSessionForUser returned null")
|
||||||
this.session = session
|
this.session = session
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
@ -114,23 +122,24 @@ internal class RestoreViewModel(
|
||||||
mRestoreSetResults.value = getAvailableRestoreSets()
|
mRestoreSetResults.value = getAvailableRestoreSets()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getAvailableRestoreSets() = suspendCoroutine<RestoreSetResult> { continuation ->
|
private suspend fun getAvailableRestoreSets() =
|
||||||
val session = try {
|
suspendCoroutine<RestoreSetResult> { continuation ->
|
||||||
getOrStartSession()
|
val session = try {
|
||||||
} catch (e: RemoteException) {
|
getOrStartSession()
|
||||||
Log.e(TAG, "Error starting new session", e)
|
} catch (e: RemoteException) {
|
||||||
continuation.resume(RestoreSetResult(app.getString(R.string.restore_set_error)))
|
Log.e(TAG, "Error starting new session", e)
|
||||||
return@suspendCoroutine
|
continuation.resume(RestoreSetResult(app.getString(R.string.restore_set_error)))
|
||||||
}
|
return@suspendCoroutine
|
||||||
|
}
|
||||||
|
|
||||||
val observer = RestoreObserver(continuation)
|
val observer = RestoreObserver(continuation)
|
||||||
val setResult = session.getAvailableRestoreSets(observer, monitor)
|
val setResult = session.getAvailableRestoreSets(observer, monitor)
|
||||||
if (setResult != 0) {
|
if (setResult != 0) {
|
||||||
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
|
Log.e(TAG, "getAvailableRestoreSets() returned non-zero value")
|
||||||
continuation.resume(RestoreSetResult(app.getString(R.string.restore_set_error)))
|
continuation.resume(RestoreSetResult(app.getString(R.string.restore_set_error)))
|
||||||
return@suspendCoroutine
|
return@suspendCoroutine
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
override fun onRestorableBackupClicked(restorableBackup: RestorableBackup) {
|
||||||
mChosenRestorableBackup.value = restorableBackup
|
mChosenRestorableBackup.value = restorableBackup
|
||||||
|
@ -144,16 +153,16 @@ internal class RestoreViewModel(
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
private fun getInstallResult(restorableBackup: RestorableBackup): LiveData<InstallResult> {
|
private fun getInstallResult(restorableBackup: RestorableBackup): LiveData<InstallResult> {
|
||||||
return apkRestore.restore(restorableBackup.token, restorableBackup.packageMetadataMap)
|
return apkRestore.restore(restorableBackup.token, restorableBackup.packageMetadataMap)
|
||||||
.onStart {
|
.onStart {
|
||||||
Log.d(TAG, "Start InstallResult Flow")
|
Log.d(TAG, "Start InstallResult Flow")
|
||||||
}.catch { e ->
|
}.catch { e ->
|
||||||
Log.d(TAG, "Exception in InstallResult Flow", e)
|
Log.d(TAG, "Exception in InstallResult Flow", e)
|
||||||
}.onCompletion { e ->
|
}.onCompletion { e ->
|
||||||
Log.d(TAG, "Completed InstallResult Flow", e)
|
Log.d(TAG, "Completed InstallResult Flow", e)
|
||||||
mNextButtonEnabled.postValue(true)
|
mNextButtonEnabled.postValue(true)
|
||||||
}
|
}
|
||||||
.flowOn(ioDispatcher)
|
.flowOn(ioDispatcher)
|
||||||
.asLiveData()
|
.asLiveData()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun onNextClicked() {
|
internal fun onNextClicked() {
|
||||||
|
@ -216,7 +225,10 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getFailedStatus(packageName: String, restorableBackup: RestorableBackup = chosenRestorableBackup.value!!): AppRestoreStatus {
|
private fun getFailedStatus(
|
||||||
|
packageName: String,
|
||||||
|
restorableBackup: RestorableBackup = chosenRestorableBackup.value!!
|
||||||
|
): AppRestoreStatus {
|
||||||
val metadata = restorableBackup.packageMetadataMap[packageName] ?: return FAILED
|
val metadata = restorableBackup.packageMetadataMap[packageName] ?: return FAILED
|
||||||
return when (metadata.state) {
|
return when (metadata.state) {
|
||||||
NO_DATA -> FAILED_NO_DATA
|
NO_DATA -> FAILED_NO_DATA
|
||||||
|
@ -267,7 +279,9 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private inner class RestoreObserver(private val continuation: Continuation<RestoreSetResult>? = null) : IRestoreObserver.Stub() {
|
private inner class RestoreObserver(
|
||||||
|
private val continuation: Continuation<RestoreSetResult>? = null
|
||||||
|
) : IRestoreObserver.Stub() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supply a list of the restore datasets available from the current transport.
|
* Supply a list of the restore datasets available from the current transport.
|
||||||
|
@ -290,20 +304,7 @@ internal class RestoreViewModel(
|
||||||
RestoreSetResult(app.getString(R.string.restore_set_error))
|
RestoreSetResult(app.getString(R.string.restore_set_error))
|
||||||
} else {
|
} else {
|
||||||
val restorableBackups = restoreSets.mapNotNull { set ->
|
val restorableBackups = restoreSets.mapNotNull { set ->
|
||||||
val metadata = backupMetadata[set.token]
|
getRestorableBackup(set, backupMetadata[set.token])
|
||||||
when {
|
|
||||||
metadata == null -> {
|
|
||||||
Log.e(TAG, "RestoreCoordinator#getAndClearBackupMetadata() has no metadata for token ${set.token}.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
metadata.time == 0L -> {
|
|
||||||
Log.d(TAG, "Ignoring RestoreSet with no last backup time: ${set.token}.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
RestorableBackup(set, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (restorableBackups.isEmpty()) RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
if (restorableBackups.isEmpty()) RestoreSetResult(app.getString(R.string.restore_set_empty_result))
|
||||||
else RestoreSetResult(restorableBackups)
|
else RestoreSetResult(restorableBackups)
|
||||||
|
@ -312,6 +313,20 @@ internal class RestoreViewModel(
|
||||||
continuation.resume(result)
|
continuation.resume(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getRestorableBackup(set: RestoreSet, metadata: BackupMetadata?) = when {
|
||||||
|
metadata == null -> {
|
||||||
|
Log.e(TAG, "No metadata for token ${set.token}.")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
metadata.time == 0L -> {
|
||||||
|
Log.d(TAG, "Ignoring RestoreSet with no last backup time: ${set.token}.")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
RestorableBackup(set, metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The restore operation has begun.
|
* The restore operation has begun.
|
||||||
*
|
*
|
||||||
|
@ -343,8 +358,8 @@ internal class RestoreViewModel(
|
||||||
*/
|
*/
|
||||||
override fun restoreFinished(result: Int) {
|
override fun restoreFinished(result: Int) {
|
||||||
val restoreResult = RestoreBackupResult(
|
val restoreResult = RestoreBackupResult(
|
||||||
if (result == 0) null
|
if (result == 0) null
|
||||||
else app.getString(R.string.restore_finished_error)
|
else app.getString(R.string.restore_finished_error)
|
||||||
)
|
)
|
||||||
onRestoreComplete(restoreResult)
|
onRestoreComplete(restoreResult)
|
||||||
closeSession()
|
closeSession()
|
||||||
|
@ -355,8 +370,9 @@ internal class RestoreViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class RestoreSetResult(
|
internal class RestoreSetResult(
|
||||||
internal val restorableBackups: List<RestorableBackup>,
|
internal val restorableBackups: List<RestorableBackup>,
|
||||||
internal val errorMsg: String?) {
|
internal val errorMsg: String?
|
||||||
|
) {
|
||||||
|
|
||||||
internal constructor(restorableBackups: List<RestorableBackup>) : this(restorableBackups, null)
|
internal constructor(restorableBackups: List<RestorableBackup>) : this(restorableBackups, null)
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,11 @@ class AboutDialogFragment : DialogFragment() {
|
||||||
internal val TAG = AboutDialogFragment::class.java.simpleName
|
internal val TAG = AboutDialogFragment::class.java.simpleName
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
override fun onCreateView(
|
||||||
savedInstanceState: Bundle?): View? {
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
val v: View = inflater.inflate(R.layout.fragment_about, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_about, container, false)
|
||||||
|
|
||||||
licenseView = v.findViewById(R.id.licenseView)
|
licenseView = v.findViewById(R.id.licenseView)
|
||||||
|
|
|
@ -22,13 +22,15 @@ import com.stevesoltys.seedvault.settings.AppStatusAdapter.AppStatusViewHolder
|
||||||
import com.stevesoltys.seedvault.ui.AppViewHolder
|
import com.stevesoltys.seedvault.ui.AppViewHolder
|
||||||
import com.stevesoltys.seedvault.ui.toRelativeTime
|
import com.stevesoltys.seedvault.ui.toRelativeTime
|
||||||
|
|
||||||
internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) : Adapter<AppStatusViewHolder>() {
|
internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListener) :
|
||||||
|
Adapter<AppStatusViewHolder>() {
|
||||||
|
|
||||||
private val items = ArrayList<AppStatus>()
|
private val items = ArrayList<AppStatus>()
|
||||||
private var editMode = false
|
private var editMode = false
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppStatusViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context).inflate(R.layout.list_item_app_status, parent, false)
|
val v = LayoutInflater.from(parent.context)
|
||||||
|
.inflate(R.layout.list_item_app_status, parent, false)
|
||||||
return AppStatusViewHolder(v)
|
return AppStatusViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,16 +105,18 @@ internal class AppStatusAdapter(private val toggleListener: AppStatusToggleListe
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AppStatus(
|
data class AppStatus(
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
var enabled: Boolean,
|
var enabled: Boolean,
|
||||||
val icon: Drawable,
|
val icon: Drawable,
|
||||||
val name: String,
|
val name: String,
|
||||||
val time: Long,
|
val time: Long,
|
||||||
val status: AppRestoreStatus)
|
val status: AppRestoreStatus
|
||||||
|
)
|
||||||
|
|
||||||
internal class AppStatusDiff(
|
internal class AppStatusDiff(
|
||||||
private val oldItems: List<AppStatus>,
|
private val oldItems: List<AppStatus>,
|
||||||
private val newItems: List<AppStatus>) : DiffUtil.Callback() {
|
private val newItems: List<AppStatus>
|
||||||
|
) : DiffUtil.Callback() {
|
||||||
|
|
||||||
override fun getOldListSize() = oldItems.size
|
override fun getOldListSize() = oldItems.size
|
||||||
override fun getNewListSize() = newItems.size
|
override fun getNewListSize() = newItems.size
|
||||||
|
@ -127,6 +131,6 @@ internal class AppStatusDiff(
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AppStatusResult(
|
internal class AppStatusResult(
|
||||||
val appStatusList: List<AppStatus>,
|
val appStatusList: List<AppStatus>,
|
||||||
val diff: DiffResult
|
val diff: DiffResult
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,8 +32,11 @@ class AppStatusFragment : Fragment(), AppStatusToggleListener {
|
||||||
private lateinit var list: RecyclerView
|
private lateinit var list: RecyclerView
|
||||||
private lateinit var progressBar: ProgressBar
|
private lateinit var progressBar: ProgressBar
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
override fun onCreateView(
|
||||||
savedInstanceState: Bundle?): View? {
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
val v: View = inflater.inflate(R.layout.fragment_app_status, container, false)
|
val v: View = inflater.inflate(R.layout.fragment_app_status, container, false)
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
addAction(ACTION_USB_DEVICE_DETACHED)
|
addAction(ACTION_USB_DEVICE_DETACHED)
|
||||||
}
|
}
|
||||||
private val usbReceiver = object : UsbMonitor() {
|
private val usbReceiver = object : UsbMonitor() {
|
||||||
override fun shouldMonitorStatus(context: Context, action: String, device: UsbDevice): Boolean {
|
override fun shouldMonitorStatus(
|
||||||
|
context: Context,
|
||||||
|
action: String,
|
||||||
|
device: UsbDevice
|
||||||
|
): Boolean {
|
||||||
return device.isMassStorage()
|
return device.isMassStorage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,17 +108,17 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val enable = newValue as Boolean
|
val enable = newValue as Boolean
|
||||||
if (enable) return@OnPreferenceChangeListener true
|
if (enable) return@OnPreferenceChangeListener true
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setIcon(R.drawable.ic_warning)
|
.setIcon(R.drawable.ic_warning)
|
||||||
.setTitle(R.string.settings_backup_apk_dialog_title)
|
.setTitle(R.string.settings_backup_apk_dialog_title)
|
||||||
.setMessage(R.string.settings_backup_apk_dialog_message)
|
.setMessage(R.string.settings_backup_apk_dialog_message)
|
||||||
.setPositiveButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ ->
|
.setPositiveButton(R.string.settings_backup_apk_dialog_cancel) { dialog, _ ->
|
||||||
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 = enable
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
return@OnPreferenceChangeListener false
|
return@OnPreferenceChangeListener false
|
||||||
}
|
}
|
||||||
backupStatus = findPreference("backup_status")!!
|
backupStatus = findPreference("backup_status")!!
|
||||||
|
@ -123,7 +127,9 @@ 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, Observer { time -> setAppBackupStatusSummary(time) })
|
viewModel.lastBackupTime.observe(viewLifecycleOwner, Observer { time ->
|
||||||
|
setAppBackupStatusSummary(time)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
@ -192,7 +198,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
val storage = this.storage
|
val storage = this.storage
|
||||||
if (storage?.isUsb == true) {
|
if (storage?.isUsb == true) {
|
||||||
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
|
autoRestore.summary = getString(R.string.settings_auto_restore_summary) + "\n\n" +
|
||||||
getString(R.string.settings_auto_restore_summary_usb, storage.name)
|
getString(R.string.settings_auto_restore_summary_usb, storage.name)
|
||||||
} else {
|
} else {
|
||||||
autoRestore.setSummary(R.string.settings_auto_restore_summary)
|
autoRestore.setSummary(R.string.settings_auto_restore_summary)
|
||||||
}
|
}
|
||||||
|
@ -214,7 +220,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
||||||
if (menuBackupNow != null && menuRestore != null) {
|
if (menuBackupNow != null && menuRestore != null) {
|
||||||
val storage = this.storage
|
val storage = this.storage
|
||||||
val enabled = storage != null &&
|
val enabled = storage != null &&
|
||||||
(!storage.isUsb || storage.getDocumentFile(context).isDirectory)
|
(!storage.isUsb || storage.getDocumentFile(context).isDirectory)
|
||||||
menuBackupNow?.isEnabled = enabled
|
menuBackupNow?.isEnabled = enabled
|
||||||
menuRestore?.isEnabled = enabled
|
menuRestore?.isEnabled = enabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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
|
||||||
import android.app.backup.BackupTransport.FLAG_USER_INITIATED
|
|
||||||
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
|
||||||
|
|
|
@ -81,7 +81,7 @@ class ApkBackup(
|
||||||
Log.d(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"Package $packageName with version $version already has a backup ($backedUpVersion)" +
|
"Package $packageName with version $version 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -358,7 +358,10 @@ internal class BackupCoordinator(
|
||||||
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(TAG, "Package $packageName was in $oldPackageState, update to $packageState")
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"Package $packageName was in $oldPackageState, update to $packageState"
|
||||||
|
)
|
||||||
plugin.getMetadataOutputStream().use {
|
plugin.getMetadataOutputStream().use {
|
||||||
metadataManager.onPackageBackupError(packageInfo, packageState, it)
|
metadataManager.onPackageBackupError(packageInfo, packageState, it)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,48 @@ import org.koin.dsl.module
|
||||||
|
|
||||||
val backupModule = module {
|
val backupModule = module {
|
||||||
single { InputFactory() }
|
single { InputFactory() }
|
||||||
single { PackageService(androidContext(), get()) }
|
single {
|
||||||
single { ApkBackup(androidContext().packageManager, get(), get()) }
|
PackageService(
|
||||||
single { KVBackup(get<BackupPlugin>().kvBackupPlugin, get(), get(), get(), get()) }
|
context = androidContext(),
|
||||||
single { FullBackup(get<BackupPlugin>().fullBackupPlugin, get(), get(), get()) }
|
backupManager = get()
|
||||||
single { BackupCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
ApkBackup(
|
||||||
|
pm = androidContext().packageManager,
|
||||||
|
settingsManager = get(),
|
||||||
|
metadataManager = get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
KVBackup(
|
||||||
|
plugin = get<BackupPlugin>().kvBackupPlugin,
|
||||||
|
inputFactory = get(),
|
||||||
|
headerWriter = get(),
|
||||||
|
crypto = get(),
|
||||||
|
nm = get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
FullBackup(
|
||||||
|
plugin = get<BackupPlugin>().fullBackupPlugin,
|
||||||
|
inputFactory = get(),
|
||||||
|
headerWriter = get(),
|
||||||
|
crypto = get()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
single {
|
||||||
|
BackupCoordinator(
|
||||||
|
context = androidContext(),
|
||||||
|
plugin = get(),
|
||||||
|
kv = get(),
|
||||||
|
full = get(),
|
||||||
|
apkBackup = get(),
|
||||||
|
clock = get(),
|
||||||
|
packageService = get(),
|
||||||
|
metadataManager = get(),
|
||||||
|
settingsManager = get(),
|
||||||
|
nm = get()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,14 @@ import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
private class FullBackupState(
|
private class FullBackupState(
|
||||||
internal val packageInfo: PackageInfo,
|
val packageInfo: PackageInfo,
|
||||||
internal val inputFileDescriptor: ParcelFileDescriptor,
|
val inputFileDescriptor: ParcelFileDescriptor,
|
||||||
internal val inputStream: InputStream,
|
val inputStream: InputStream,
|
||||||
internal var outputStreamInit: (suspend () -> OutputStream)?
|
var outputStreamInit: (suspend () -> OutputStream)?
|
||||||
) {
|
) {
|
||||||
internal var outputStream: OutputStream? = null
|
var outputStream: OutputStream? = null
|
||||||
internal val packageName: String = packageInfo.packageName
|
val packageName: String = packageInfo.packageName
|
||||||
internal var size: Long = 0
|
var size: Long = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
|
const val DEFAULT_QUOTA_FULL_BACKUP = (2 * (25 * 1024 * 1024)).toLong()
|
||||||
|
|
|
@ -83,7 +83,7 @@ internal class KVBackup(
|
||||||
if (isIncremental && !hasDataForPackage) {
|
if (isIncremental && !hasDataForPackage) {
|
||||||
Log.w(
|
Log.w(
|
||||||
TAG, "Requested incremental, but transport currently stores no data" +
|
TAG, "Requested incremental, but transport currently stores no data" +
|
||||||
" for $packageName, requesting non-incremental retry."
|
" for $packageName, requesting non-incremental retry."
|
||||||
)
|
)
|
||||||
return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)
|
return backupError(TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED)
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,8 +73,8 @@ internal class PackageService(
|
||||||
return packageManager.getInstalledPackages(GET_SIGNING_CERTIFICATES)
|
return packageManager.getInstalledPackages(GET_SIGNING_CERTIFICATES)
|
||||||
.filter { packageInfo ->
|
.filter { packageInfo ->
|
||||||
packageInfo.doesNotGetBackedUp() && // only apps that do not allow backup
|
packageInfo.doesNotGetBackedUp() && // only apps that do not allow backup
|
||||||
!packageInfo.isNotUpdatedSystemApp() && // and are not vanilla system apps
|
!packageInfo.isNotUpdatedSystemApp() && // and are not vanilla system apps
|
||||||
packageInfo.packageName != context.packageName // not this app
|
packageInfo.packageName != context.packageName // not this app
|
||||||
}.sortedBy { packageInfo ->
|
}.sortedBy { packageInfo ->
|
||||||
packageInfo.packageName
|
packageInfo.packageName
|
||||||
}.also { notAllowed ->
|
}.also { notAllowed ->
|
||||||
|
@ -155,7 +155,7 @@ internal fun PackageInfo.isNotUpdatedSystemApp(): Boolean {
|
||||||
internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
|
internal fun PackageInfo.doesNotGetBackedUp(): Boolean {
|
||||||
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
|
if (packageName == MAGIC_PACKAGE_MANAGER || applicationInfo == null) return true
|
||||||
return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup
|
return applicationInfo.flags and FLAG_ALLOW_BACKUP == 0 || // does not allow backup
|
||||||
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
|
applicationInfo.flags and FLAG_STOPPED != 0 // is stopped
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun PackageInfo.isStopped(): Boolean {
|
internal fun PackageInfo.isStopped(): Boolean {
|
||||||
|
|
|
@ -36,7 +36,12 @@ internal class ApkInstaller(private val context: Context) {
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
internal fun install(cachedApk: File, packageName: String, installerPackageName: String?, installResult: MutableInstallResult) = callbackFlow {
|
internal fun install(
|
||||||
|
cachedApk: File,
|
||||||
|
packageName: String,
|
||||||
|
installerPackageName: String?,
|
||||||
|
installResult: MutableInstallResult
|
||||||
|
) = callbackFlow {
|
||||||
val broadcastReceiver = object : BroadcastReceiver() {
|
val broadcastReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, i: Intent) {
|
override fun onReceive(context: Context, i: Intent) {
|
||||||
if (i.action != BROADCAST_ACTION) return
|
if (i.action != BROADCAST_ACTION) return
|
||||||
|
@ -76,11 +81,17 @@ internal class ApkInstaller(private val context: Context) {
|
||||||
flags = FLAG_RECEIVER_FOREGROUND
|
flags = FLAG_RECEIVER_FOREGROUND
|
||||||
setPackage(context.packageName)
|
setPackage(context.packageName)
|
||||||
}
|
}
|
||||||
val pendingIntent = PendingIntent.getBroadcast(context, 0, broadcastIntent, FLAG_UPDATE_CURRENT)
|
val pendingIntent =
|
||||||
|
PendingIntent.getBroadcast(context, 0, broadcastIntent, FLAG_UPDATE_CURRENT)
|
||||||
return pendingIntent.intentSender
|
return pendingIntent.intentSender
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBroadcastReceived(i: Intent, expectedPackageName: String, cachedApk: File, installResult: MutableInstallResult): InstallResult {
|
private fun onBroadcastReceived(
|
||||||
|
i: Intent,
|
||||||
|
expectedPackageName: String,
|
||||||
|
cachedApk: File,
|
||||||
|
installResult: MutableInstallResult
|
||||||
|
): InstallResult {
|
||||||
val packageName = i.getStringExtra(EXTRA_PACKAGE_NAME)!!
|
val packageName = i.getStringExtra(EXTRA_PACKAGE_NAME)!!
|
||||||
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)!!
|
||||||
|
|
|
@ -23,13 +23,13 @@ import java.io.IOException
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
|
||||||
private val TAG = ApkRestore::class.java.simpleName
|
private val TAG = ApkRestore::class.java.simpleName
|
||||||
|
|
||||||
internal class ApkRestore(
|
internal class ApkRestore(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val restorePlugin: RestorePlugin,
|
private val restorePlugin: RestorePlugin,
|
||||||
private val apkInstaller: ApkInstaller = ApkInstaller(context)) {
|
private val apkInstaller: ApkInstaller = ApkInstaller(context)
|
||||||
|
) {
|
||||||
|
|
||||||
private val pm = context.packageManager
|
private val pm = context.packageManager
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ internal class ApkRestore(
|
||||||
// restore individual packages and emit updates
|
// restore individual packages and emit updates
|
||||||
for ((packageName, metadata) in packages) {
|
for ((packageName, metadata) in packages) {
|
||||||
try {
|
try {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
||||||
restore(token, packageName, metadata, installResult).collect {
|
restore(token, packageName, metadata, installResult).collect {
|
||||||
emit(it)
|
emit(it)
|
||||||
}
|
}
|
||||||
|
@ -69,9 +69,14 @@ internal class ApkRestore(
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
@Suppress("BlockingMethodInNonBlockingContext") // flows on Dispatcher.IO
|
||||||
@Throws(IOException::class, SecurityException::class)
|
@Throws(IOException::class, SecurityException::class)
|
||||||
private fun restore(token: Long, packageName: String, metadata: PackageMetadata, installResult: MutableInstallResult) = flow {
|
private fun restore(
|
||||||
|
token: Long,
|
||||||
|
packageName: String,
|
||||||
|
metadata: PackageMetadata,
|
||||||
|
installResult: MutableInstallResult
|
||||||
|
) = flow {
|
||||||
// create a cache file to write the APK into
|
// create a cache file to write the APK into
|
||||||
val cachedApk = File.createTempFile(packageName, ".apk", context.cacheDir)
|
val cachedApk = File.createTempFile(packageName, ".apk", context.cacheDir)
|
||||||
// copy APK to cache file and calculate SHA-256 hash while we are at it
|
// copy APK to cache file and calculate SHA-256 hash while we are at it
|
||||||
|
@ -97,7 +102,7 @@ internal class ApkRestore(
|
||||||
// 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
|
||||||
val packageInfo = pm.getPackageArchiveInfo(cachedApk.absolutePath, flags)
|
val packageInfo = pm.getPackageArchiveInfo(cachedApk.absolutePath, flags)
|
||||||
?: 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) {
|
||||||
|
@ -106,7 +111,10 @@ internal class ApkRestore(
|
||||||
|
|
||||||
// check APK version code
|
// check APK version code
|
||||||
if (metadata.version != packageInfo.longVersionCode) {
|
if (metadata.version != packageInfo.longVersionCode) {
|
||||||
Log.w(TAG, "Package $packageName expects version code ${metadata.version}, but has ${packageInfo.longVersionCode}.")
|
Log.w(
|
||||||
|
TAG, "Package $packageName expects version code ${metadata.version}," +
|
||||||
|
"but has ${packageInfo.longVersionCode}."
|
||||||
|
)
|
||||||
// TODO should we let this one pass, maybe once we can revert PackageMetadata during backup?
|
// TODO should we let this one pass, maybe once we can revert PackageMetadata during backup?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +133,13 @@ internal class ApkRestore(
|
||||||
val icon = appInfo.loadIcon(pm)
|
val icon = appInfo.loadIcon(pm)
|
||||||
val name = pm.getApplicationLabel(appInfo)
|
val name = pm.getApplicationLabel(appInfo)
|
||||||
|
|
||||||
installResult.update(packageName) { it.copy(status = IN_PROGRESS, name = name, icon = icon) }
|
installResult.update(packageName) { result ->
|
||||||
|
result.copy(
|
||||||
|
status = IN_PROGRESS,
|
||||||
|
name = name,
|
||||||
|
icon = icon
|
||||||
|
)
|
||||||
|
}
|
||||||
emit(installResult)
|
emit(installResult)
|
||||||
|
|
||||||
// ensure system apps are actually installed and newer system apps as well
|
// ensure system apps are actually installed and newer system apps as well
|
||||||
|
@ -143,9 +157,10 @@ internal class ApkRestore(
|
||||||
}
|
}
|
||||||
|
|
||||||
// install APK and emit updates from it
|
// install APK and emit updates from it
|
||||||
apkInstaller.install(cachedApk, packageName, metadata.installer, installResult).collect { result ->
|
apkInstaller.install(cachedApk, packageName, metadata.installer, installResult)
|
||||||
emit(result)
|
.collect { result ->
|
||||||
}
|
emit(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fail(installResult: MutableInstallResult, packageName: String): InstallResult {
|
private fun fail(installResult: MutableInstallResult, packageName: String): InstallResult {
|
||||||
|
@ -163,8 +178,12 @@ internal fun InstallResult.getInProgress(): ApkRestoreResult? {
|
||||||
return filtered.values.first()
|
return filtered.values.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class MutableInstallResult(initialCapacity: Int) : ConcurrentHashMap<String, ApkRestoreResult>(initialCapacity) {
|
internal class MutableInstallResult(initialCapacity: Int) :
|
||||||
fun update(packageName: String, updateFun: (ApkRestoreResult) -> ApkRestoreResult): MutableInstallResult {
|
ConcurrentHashMap<String, ApkRestoreResult>(initialCapacity) {
|
||||||
|
fun update(
|
||||||
|
packageName: String,
|
||||||
|
updateFun: (ApkRestoreResult) -> ApkRestoreResult
|
||||||
|
): MutableInstallResult {
|
||||||
val result = get(packageName)
|
val result = get(packageName)
|
||||||
check(result != null) { "ApkRestoreResult for $packageName does not exist." }
|
check(result != null) { "ApkRestoreResult for $packageName does not exist." }
|
||||||
set(packageName, updateFun(result))
|
set(packageName, updateFun(result))
|
||||||
|
@ -173,12 +192,12 @@ internal class MutableInstallResult(initialCapacity: Int) : ConcurrentHashMap<St
|
||||||
}
|
}
|
||||||
|
|
||||||
internal data class ApkRestoreResult(
|
internal data class ApkRestoreResult(
|
||||||
val packageName: CharSequence,
|
val packageName: CharSequence,
|
||||||
val progress: Int,
|
val progress: Int,
|
||||||
val total: Int,
|
val total: Int,
|
||||||
val status: ApkRestoreStatus,
|
val status: ApkRestoreStatus,
|
||||||
val name: CharSequence? = null,
|
val name: CharSequence? = null,
|
||||||
val icon: Drawable? = null
|
val icon: Drawable? = null
|
||||||
) : Comparable<ApkRestoreResult> {
|
) : Comparable<ApkRestoreResult> {
|
||||||
override fun compareTo(other: ApkRestoreResult): Int {
|
override fun compareTo(other: ApkRestoreResult): Int {
|
||||||
return other.progress.compareTo(progress)
|
return other.progress.compareTo(progress)
|
||||||
|
|
|
@ -16,19 +16,21 @@ import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
private class FullRestoreState(
|
private class FullRestoreState(
|
||||||
internal val token: Long,
|
val token: Long,
|
||||||
internal val packageInfo: PackageInfo) {
|
val packageInfo: PackageInfo
|
||||||
internal var inputStream: InputStream? = null
|
) {
|
||||||
|
var inputStream: InputStream? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = FullRestore::class.java.simpleName
|
private val TAG = FullRestore::class.java.simpleName
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
internal class FullRestore(
|
internal class FullRestore(
|
||||||
private val plugin: FullRestorePlugin,
|
private val plugin: FullRestorePlugin,
|
||||||
private val outputFactory: OutputFactory,
|
private val outputFactory: OutputFactory,
|
||||||
private val headerReader: HeaderReader,
|
private val headerReader: HeaderReader,
|
||||||
private val crypto: Crypto) {
|
private val crypto: Crypto
|
||||||
|
) {
|
||||||
|
|
||||||
private var state: FullRestoreState? = null
|
private var state: FullRestoreState? = null
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@ import java.util.ArrayList
|
||||||
import javax.crypto.AEADBadTagException
|
import javax.crypto.AEADBadTagException
|
||||||
|
|
||||||
private class KVRestoreState(
|
private class KVRestoreState(
|
||||||
internal val token: Long,
|
val token: Long,
|
||||||
internal val packageInfo: PackageInfo,
|
val packageInfo: PackageInfo,
|
||||||
/**
|
/**
|
||||||
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
|
* Optional [PackageInfo] for single package restore, optimizes restore of @pm@
|
||||||
*/
|
*/
|
||||||
internal val pmPackageInfo: PackageInfo?
|
val pmPackageInfo: PackageInfo?
|
||||||
)
|
)
|
||||||
|
|
||||||
private val TAG = KVRestore::class.java.simpleName
|
private val TAG = KVRestore::class.java.simpleName
|
||||||
|
@ -156,8 +156,8 @@ internal class KVRestore(
|
||||||
Unit
|
Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DecodedKey(internal val base64Key: String) : Comparable<DecodedKey> {
|
private class DecodedKey(val base64Key: String) : Comparable<DecodedKey> {
|
||||||
internal val key = base64Key.decodeBase64()
|
val key = base64Key.decodeBase64()
|
||||||
|
|
||||||
override fun compareTo(other: DecodedKey) = key.compareTo(other.key)
|
override fun compareTo(other: DecodedKey) = key.compareTo(other.key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,10 @@ interface KVRestorePlugin {
|
||||||
* Note: Implementations might expect that you call [hasDataForPackage] before.
|
* Note: Implementations might expect that you call [hasDataForPackage] before.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
suspend fun getInputStreamForRecord(token: Long, packageInfo: PackageInfo, key: String): InputStream
|
suspend fun getInputStreamForRecord(
|
||||||
|
token: Long,
|
||||||
|
packageInfo: PackageInfo,
|
||||||
|
key: String
|
||||||
|
): InputStream
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ internal class OutputFactory {
|
||||||
fun getBackupDataOutput(outputFileDescriptor: ParcelFileDescriptor): BackupDataOutput {
|
fun getBackupDataOutput(outputFileDescriptor: ParcelFileDescriptor): BackupDataOutput {
|
||||||
return BackupDataOutput(outputFileDescriptor.fileDescriptor)
|
return BackupDataOutput(outputFileDescriptor.fileDescriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
|
fun getOutputStream(outputFileDescriptor: ParcelFileDescriptor): OutputStream {
|
||||||
return FileOutputStream(outputFileDescriptor.fileDescriptor)
|
return FileOutputStream(outputFileDescriptor.fileDescriptor)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ abstract class BackupActivity : AppCompatActivity() {
|
||||||
|
|
||||||
protected fun showFragment(f: Fragment, addToBackStack: Boolean = false) {
|
protected fun showFragment(f: Fragment, addToBackStack: Boolean = false) {
|
||||||
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.fragment, f)
|
.replace(R.id.fragment, f)
|
||||||
if (addToBackStack) fragmentTransaction.addToBackStack(null)
|
if (addToBackStack) fragmentTransaction.addToBackStack(null)
|
||||||
fragmentTransaction.commit()
|
fragmentTransaction.commit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,8 @@ open class LiveEvent<T> : LiveData<ConsumableEvent<T>>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class LiveEventObserver<T>(private val handler: LiveEventHandler<in T>) : Observer<ConsumableEvent<T>> {
|
internal class LiveEventObserver<T>(private val handler: LiveEventHandler<in T>) :
|
||||||
|
Observer<ConsumableEvent<T>> {
|
||||||
override fun onChanged(consumableEvent: ConsumableEvent<T>?) {
|
override fun onChanged(consumableEvent: ConsumableEvent<T>?) {
|
||||||
if (consumableEvent != null) {
|
if (consumableEvent != null) {
|
||||||
val content = consumableEvent.contentIfNotConsumed
|
val content = consumableEvent.contentIfNotConsumed
|
||||||
|
|
|
@ -7,9 +7,9 @@ import com.stevesoltys.seedvault.settings.SettingsManager
|
||||||
import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
import com.stevesoltys.seedvault.ui.storage.StorageViewModel
|
||||||
|
|
||||||
abstract class RequireProvisioningViewModel(
|
abstract class RequireProvisioningViewModel(
|
||||||
protected val app: Application,
|
protected val app: Application,
|
||||||
protected val settingsManager: SettingsManager,
|
protected val settingsManager: SettingsManager,
|
||||||
private val keyManager: KeyManager
|
private val keyManager: KeyManager
|
||||||
) : AndroidViewModel(app) {
|
) : AndroidViewModel(app) {
|
||||||
|
|
||||||
abstract val isRestoreOperation: Boolean
|
abstract val isRestoreOperation: Boolean
|
||||||
|
|
|
@ -49,14 +49,14 @@ class RecoveryCodeActivity : BackupActivity() {
|
||||||
|
|
||||||
private fun showOutput() {
|
private fun showOutput() {
|
||||||
supportFragmentManager.beginTransaction()
|
supportFragmentManager.beginTransaction()
|
||||||
.add(R.id.fragment, RecoveryCodeOutputFragment(), "Code")
|
.add(R.id.fragment, RecoveryCodeOutputFragment(), "Code")
|
||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showInput(addToBackStack: Boolean) {
|
private fun showInput(addToBackStack: Boolean) {
|
||||||
val tag = "Confirm"
|
val tag = "Confirm"
|
||||||
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
val fragmentTransaction = supportFragmentManager.beginTransaction()
|
||||||
.replace(R.id.fragment, RecoveryCodeInputFragment(), tag)
|
.replace(R.id.fragment, RecoveryCodeInputFragment(), tag)
|
||||||
if (addToBackStack) fragmentTransaction.addToBackStack(tag)
|
if (addToBackStack) fragmentTransaction.addToBackStack(tag)
|
||||||
fragmentTransaction.commit()
|
fragmentTransaction.commit()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,12 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter
|
import androidx.recyclerview.widget.RecyclerView.Adapter
|
||||||
import com.stevesoltys.seedvault.R
|
import com.stevesoltys.seedvault.R
|
||||||
|
|
||||||
class RecoveryCodeAdapter(private val items: List<CharSequence>) : Adapter<RecoveryCodeViewHolder>() {
|
class RecoveryCodeAdapter(private val items: List<CharSequence>) :
|
||||||
|
Adapter<RecoveryCodeViewHolder>() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecoveryCodeViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecoveryCodeViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_recovery_code_output, parent, false) as View
|
.inflate(R.layout.list_item_recovery_code_output, parent, false) as View
|
||||||
return RecoveryCodeViewHolder(v)
|
return RecoveryCodeViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,7 +130,8 @@ class RecoveryCodeInputFragment : Fragment() {
|
||||||
private fun showWrongWordError(input: List<CharSequence>, e: WordNotFoundException) {
|
private fun showWrongWordError(input: List<CharSequence>, e: WordNotFoundException) {
|
||||||
val i = input.indexOf(e.word)
|
val i = input.indexOf(e.word)
|
||||||
if (i == -1) throw AssertionError()
|
if (i == -1) throw AssertionError()
|
||||||
showError(i, getString(R.string.recovery_code_error_invalid_word, e.suggestion1, e.suggestion2))
|
val str = getString(R.string.recovery_code_error_invalid_word, e.suggestion1, e.suggestion2)
|
||||||
|
showError(i, str)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showError(i: Int, errorMsg: CharSequence) {
|
private fun showError(i: Int, errorMsg: CharSequence) {
|
||||||
|
|
|
@ -15,9 +15,10 @@ import java.io.IOException
|
||||||
private val TAG = RestoreStorageViewModel::class.java.simpleName
|
private val TAG = RestoreStorageViewModel::class.java.simpleName
|
||||||
|
|
||||||
internal class RestoreStorageViewModel(
|
internal class RestoreStorageViewModel(
|
||||||
private val app: Application,
|
private val app: Application,
|
||||||
private val restorePlugin: RestorePlugin,
|
private val restorePlugin: RestorePlugin,
|
||||||
settingsManager: SettingsManager) : StorageViewModel(app, settingsManager) {
|
settingsManager: SettingsManager
|
||||||
|
) : StorageViewModel(app, settingsManager) {
|
||||||
|
|
||||||
override val isRestoreOperation = true
|
override val isRestoreOperation = true
|
||||||
|
|
||||||
|
@ -37,7 +38,8 @@ internal class RestoreStorageViewModel(
|
||||||
Log.w(TAG, "Location was rejected: $uri")
|
Log.w(TAG, "Location was rejected: $uri")
|
||||||
|
|
||||||
// notify the UI that the location was invalid
|
// notify the UI that the location was invalid
|
||||||
val errorMsg = app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT)
|
val errorMsg =
|
||||||
|
app.getString(R.string.restore_invalid_location_message, DIRECTORY_ROOT)
|
||||||
mLocationChecked.postEvent(LocationResult(errorMsg))
|
mLocationChecked.postEvent(LocationResult(errorMsg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,10 +63,10 @@ class StorageActivity : BackupActivity() {
|
||||||
if (viewModel.isRestoreOperation) {
|
if (viewModel.isRestoreOperation) {
|
||||||
supportFragmentManager.popBackStack()
|
supportFragmentManager.popBackStack()
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(getString(R.string.restore_invalid_location_title))
|
.setTitle(getString(R.string.restore_invalid_location_title))
|
||||||
.setMessage(errorMsg)
|
.setMessage(errorMsg)
|
||||||
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
.setPositiveButton(android.R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle(), errorMsg))
|
showFragment(StorageCheckFragment.newInstance(getCheckFragmentTitle(), errorMsg))
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,15 @@ import com.stevesoltys.seedvault.R
|
||||||
import com.stevesoltys.seedvault.ui.storage.StorageRootAdapter.StorageRootViewHolder
|
import com.stevesoltys.seedvault.ui.storage.StorageRootAdapter.StorageRootViewHolder
|
||||||
|
|
||||||
internal class StorageRootAdapter(
|
internal class StorageRootAdapter(
|
||||||
private val isRestore: Boolean,
|
private val isRestore: Boolean,
|
||||||
private val listener: StorageRootClickedListener) : Adapter<StorageRootViewHolder>() {
|
private val listener: StorageRootClickedListener
|
||||||
|
) : Adapter<StorageRootViewHolder>() {
|
||||||
|
|
||||||
private val items = ArrayList<StorageRoot>()
|
private val items = ArrayList<StorageRoot>()
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StorageRootViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StorageRootViewHolder {
|
||||||
val v = LayoutInflater.from(parent.context)
|
val v = LayoutInflater.from(parent.context)
|
||||||
.inflate(R.layout.list_item_storage_root, parent, false) as View
|
.inflate(R.layout.list_item_storage_root, parent, false) as View
|
||||||
return StorageRootViewHolder(v)
|
return StorageRootViewHolder(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,16 +85,16 @@ internal class StorageRootAdapter(
|
||||||
|
|
||||||
private fun showWarningDialog(context: Context, item: StorageRoot) {
|
private fun showWarningDialog(context: Context, item: StorageRoot) {
|
||||||
AlertDialog.Builder(context)
|
AlertDialog.Builder(context)
|
||||||
.setTitle(R.string.storage_internal_warning_title)
|
.setTitle(R.string.storage_internal_warning_title)
|
||||||
.setMessage(R.string.storage_internal_warning_message)
|
.setMessage(R.string.storage_internal_warning_message)
|
||||||
.setPositiveButton(R.string.storage_internal_warning_choose_other) { dialog, _ ->
|
.setPositiveButton(R.string.storage_internal_warning_choose_other) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.storage_internal_warning_use_anyway) { dialog, _ ->
|
.setNegativeButton(R.string.storage_internal_warning_use_anyway) { dialog, _ ->
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
listener.onClick(item)
|
listener.onClick(item)
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,16 +42,17 @@ private const val NEXTCLOUD_PACKAGE = "com.nextcloud.client"
|
||||||
private const val NEXTCLOUD_ACTIVITY = "com.owncloud.android.authentication.AuthenticatorActivity"
|
private const val NEXTCLOUD_ACTIVITY = "com.owncloud.android.authentication.AuthenticatorActivity"
|
||||||
|
|
||||||
data class StorageRoot(
|
data class StorageRoot(
|
||||||
internal val authority: String,
|
internal val authority: String,
|
||||||
internal val rootId: String,
|
internal val rootId: String,
|
||||||
internal val documentId: String,
|
internal val documentId: String,
|
||||||
internal val icon: Drawable?,
|
internal val icon: Drawable?,
|
||||||
internal val title: String,
|
internal val title: String,
|
||||||
internal val summary: String?,
|
internal val summary: String?,
|
||||||
internal val availableBytes: Long?,
|
internal val availableBytes: Long?,
|
||||||
internal val isUsb: Boolean,
|
internal val isUsb: Boolean,
|
||||||
internal val enabled: Boolean = true,
|
internal val enabled: Boolean = true,
|
||||||
internal val overrideClickListener: (() -> Unit)? = null) {
|
internal val overrideClickListener: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
|
||||||
internal val uri: Uri by lazy {
|
internal val uri: Uri by lazy {
|
||||||
DocumentsContract.buildTreeDocumentUri(authority, documentId)
|
DocumentsContract.buildTreeDocumentUri(authority, documentId)
|
||||||
|
@ -70,7 +71,8 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
|
|
||||||
private val packageManager = context.packageManager
|
private val packageManager = context.packageManager
|
||||||
private val contentResolver = context.contentResolver
|
private val contentResolver = context.contentResolver
|
||||||
private val whitelistedAuthorities = context.resources.getStringArray(R.array.storage_authority_whitelist)
|
private val whitelistedAuthorities =
|
||||||
|
context.resources.getStringArray(R.array.storage_authority_whitelist)
|
||||||
|
|
||||||
private var listener: RemovableStorageListener? = null
|
private var listener: RemovableStorageListener? = null
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
@ -143,17 +145,17 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
val rootId = cursor.getString(COLUMN_ROOT_ID)!!
|
val rootId = cursor.getString(COLUMN_ROOT_ID)!!
|
||||||
if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null
|
if (authority == AUTHORITY_STORAGE && rootId == ROOT_ID_HOME) return null
|
||||||
return StorageRoot(
|
return StorageRoot(
|
||||||
authority = authority,
|
authority = authority,
|
||||||
rootId = rootId,
|
rootId = rootId,
|
||||||
documentId = cursor.getString(COLUMN_DOCUMENT_ID)!!,
|
documentId = cursor.getString(COLUMN_DOCUMENT_ID)!!,
|
||||||
icon = getIcon(context, authority, rootId, cursor.getInt(COLUMN_ICON)),
|
icon = getIcon(context, authority, rootId, cursor.getInt(COLUMN_ICON)),
|
||||||
title = cursor.getString(COLUMN_TITLE)!!,
|
title = cursor.getString(COLUMN_TITLE)!!,
|
||||||
summary = cursor.getString(COLUMN_SUMMARY),
|
summary = cursor.getString(COLUMN_SUMMARY),
|
||||||
availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES).let { bytes ->
|
availableBytes = cursor.getLong(COLUMN_AVAILABLE_BYTES).let { bytes ->
|
||||||
// AOSP 11 reports -1 instead of null
|
// AOSP 11 reports -1 instead of null
|
||||||
if (bytes == -1L) null else bytes
|
if (bytes == -1L) null else bytes
|
||||||
},
|
},
|
||||||
isUsb = flags and FLAG_REMOVABLE_USB != 0
|
isUsb = flags and FLAG_REMOVABLE_USB != 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,15 +167,15 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
if (root.authority == AUTHORITY_STORAGE && root.isUsb) return
|
if (root.authority == AUTHORITY_STORAGE && root.isUsb) return
|
||||||
}
|
}
|
||||||
val root = StorageRoot(
|
val root = StorageRoot(
|
||||||
authority = AUTHORITY_STORAGE,
|
authority = AUTHORITY_STORAGE,
|
||||||
rootId = "usb",
|
rootId = "usb",
|
||||||
documentId = "fake",
|
documentId = "fake",
|
||||||
icon = getIcon(context, AUTHORITY_STORAGE, "usb", 0),
|
icon = getIcon(context, AUTHORITY_STORAGE, "usb", 0),
|
||||||
title = context.getString(R.string.storage_fake_drive_title),
|
title = context.getString(R.string.storage_fake_drive_title),
|
||||||
summary = context.getString(R.string.storage_fake_drive_summary),
|
summary = context.getString(R.string.storage_fake_drive_summary),
|
||||||
availableBytes = null,
|
availableBytes = null,
|
||||||
isUsb = true,
|
isUsb = true,
|
||||||
enabled = false
|
enabled = false
|
||||||
)
|
)
|
||||||
roots.add(root)
|
roots.add(root)
|
||||||
}
|
}
|
||||||
|
@ -206,24 +208,24 @@ internal class StorageRootFetcher(private val context: Context, private val isRe
|
||||||
else R.string.storage_fake_nextcloud_summary_unavailable
|
else R.string.storage_fake_nextcloud_summary_unavailable
|
||||||
} else R.string.storage_fake_nextcloud_summary
|
} else R.string.storage_fake_nextcloud_summary
|
||||||
val root = StorageRoot(
|
val root = StorageRoot(
|
||||||
authority = AUTHORITY_NEXTCLOUD,
|
authority = AUTHORITY_NEXTCLOUD,
|
||||||
rootId = "fake",
|
rootId = "fake",
|
||||||
documentId = "fake",
|
documentId = "fake",
|
||||||
icon = getIcon(context, AUTHORITY_NEXTCLOUD, "fake", 0),
|
icon = getIcon(context, AUTHORITY_NEXTCLOUD, "fake", 0),
|
||||||
title = context.getString(R.string.storage_fake_nextcloud_title),
|
title = context.getString(R.string.storage_fake_nextcloud_title),
|
||||||
summary = context.getString(summaryRes),
|
summary = context.getString(summaryRes),
|
||||||
availableBytes = null,
|
availableBytes = null,
|
||||||
isUsb = false,
|
isUsb = false,
|
||||||
enabled = !isInstalled || isRestore,
|
enabled = !isInstalled || isRestore,
|
||||||
overrideClickListener = {
|
overrideClickListener = {
|
||||||
if (isInstalled) context.startActivity(intent)
|
if (isInstalled) context.startActivity(intent)
|
||||||
else {
|
else {
|
||||||
val uri = Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE")
|
val uri = Uri.parse("market://details?id=$NEXTCLOUD_PACKAGE")
|
||||||
val i = Intent(ACTION_VIEW, uri)
|
val i = Intent(ACTION_VIEW, uri)
|
||||||
i.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
i.addFlags(FLAG_ACTIVITY_NEW_TASK)
|
||||||
context.startActivity(i)
|
context.startActivity(i)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
roots.add(root)
|
roots.add(root)
|
||||||
}
|
}
|
||||||
|
|
|
@ -123,7 +123,7 @@ private class OpenSeedvaultTree : OpenDocumentTree() {
|
||||||
check(input != null) { "Uri was null, but is needed." }
|
check(input != null) { "Uri was null, but is needed." }
|
||||||
data = input
|
data = input
|
||||||
val flags = FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
val flags = FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||||
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
addFlags(flags)
|
addFlags(flags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
<!--
|
|
||||||
Nextcloud Android client application
|
Nextcloud Android client application
|
||||||
|
|
||||||
Copyright (C) 2017 Andy Scherzinger
|
Copyright (C) 2017 Andy Scherzinger
|
||||||
|
@ -19,6 +18,6 @@
|
||||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/nextcloud_background"/>
|
<background android:drawable="@drawable/nextcloud_background" />
|
||||||
<foreground android:drawable="@drawable/nextcloud_foreground"/>
|
<foreground android:drawable="@drawable/nextcloud_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -18,15 +18,16 @@
|
||||||
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="108dp"
|
android:width="108dp"
|
||||||
android:height="108dp"
|
android:height="108dp"
|
||||||
android:viewportWidth="1636.9231"
|
android:viewportWidth="1636.9231"
|
||||||
android:viewportHeight="1636.9231">
|
android:viewportHeight="1636.9231">
|
||||||
<group android:translateX="286.46155"
|
<group
|
||||||
android:translateY="286.46155">
|
android:translateX="286.46155"
|
||||||
|
android:translateY="286.46155">
|
||||||
<path
|
<path
|
||||||
android:pathData="M532.7,320C439.3,320 360.9,383.9 337,469.9 316.1,423.9 270.1,391.3 216.6,391.3 143.8,391.3 84,451.2 84,524c-0,72.8 59.8,132.6 132.6,132.7 53.5,-0 99.4,-32.6 120.4,-78.6 23.9,86 102.4,149.9 195.7,149.9 92.8,0 170.8,-63.2 195.3,-148.5 21.2,45.1 66.5,77.2 119.4,77.2 72.8,0 132.7,-59.8 132.6,-132.7 -0,-72.8 -59.9,-132.6 -132.6,-132.6 -52.8,0 -98.2,32 -119.4,77.2 -24.4,-85.3 -102.4,-148.5 -195.3,-148.5zM532.7,397.9c70.1,0 126.1,56 126.1,126.1 0,70.1 -56,126.1 -126.1,126.1 -70.1,-0 -126.1,-56 -126.1,-126.1 0,-70.1 56,-126.1 126.1,-126.1zM216.6,469.2c30.7,0 54.8,24.1 54.8,54.8 0,30.7 -24,54.8 -54.8,54.8 -30.7,0 -54.8,-24.1 -54.8,-54.8 0,-30.7 24.1,-54.8 54.8,-54.8zM847.4,469.2c30.7,-0 54.8,24.1 54.8,54.8 0,30.7 -24.1,54.8 -54.8,54.8 -30.7,0 -54.8,-24.1 -54.8,-54.8 0,-30.7 24.1,-54.8 54.8,-54.8z"
|
android:fillColor="#ffffff"
|
||||||
android:fillType="nonZero"
|
android:fillType="nonZero"
|
||||||
android:fillColor="#ffffff"/>
|
android:pathData="M532.7,320C439.3,320 360.9,383.9 337,469.9 316.1,423.9 270.1,391.3 216.6,391.3 143.8,391.3 84,451.2 84,524c-0,72.8 59.8,132.6 132.6,132.7 53.5,-0 99.4,-32.6 120.4,-78.6 23.9,86 102.4,149.9 195.7,149.9 92.8,0 170.8,-63.2 195.3,-148.5 21.2,45.1 66.5,77.2 119.4,77.2 72.8,0 132.7,-59.8 132.6,-132.7 -0,-72.8 -59.9,-132.6 -132.6,-132.6 -52.8,0 -98.2,32 -119.4,77.2 -24.4,-85.3 -102.4,-148.5 -195.3,-148.5zM532.7,397.9c70.1,0 126.1,56 126.1,126.1 0,70.1 -56,126.1 -126.1,126.1 -70.1,-0 -126.1,-56 -126.1,-126.1 0,-70.1 56,-126.1 126.1,-126.1zM216.6,469.2c30.7,0 54.8,24.1 54.8,54.8 0,30.7 -24,54.8 -54.8,54.8 -30.7,0 -54.8,-24.1 -54.8,-54.8 0,-30.7 24.1,-54.8 54.8,-54.8zM847.4,469.2c30.7,-0 54.8,24.1 54.8,54.8 0,30.7 -24.1,54.8 -54.8,54.8 -30.7,0 -54.8,-24.1 -54.8,-54.8 0,-30.7 24.1,-54.8 54.8,-54.8z" />
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
@ -81,9 +81,9 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/doneButton"
|
app:layout_constraintEnd_toStartOf="@+id/doneButton"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
app:layout_constraintVertical_bias="1.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/wordList"
|
app:layout_constraintTop_toBottomOf="@+id/wordList"
|
||||||
|
app:layout_constraintVertical_bias="1.0"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -62,8 +62,8 @@
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/button"
|
app:layout_constraintBottom_toTopOf="@+id/button"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/backupNameView"
|
||||||
tools:listitem="@layout/list_item_app_status" />
|
tools:listitem="@layout/list_item_app_status" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -76,8 +76,8 @@
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:text="@string/restore_next"
|
android:text="@string/restore_next"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="1.0"
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
|
|
@ -55,8 +55,8 @@
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/errorView"
|
android:id="@+id/errorView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_margin="16dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
android:textColor="@android:color/holo_red_dark"
|
android:textColor="@android:color/holo_red_dark"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
|
|
|
@ -25,8 +25,8 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:textSize="24sp"
|
|
||||||
android:autoSizeTextType="uniform"
|
android:autoSizeTextType="uniform"
|
||||||
|
android:textSize="24sp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/num"
|
app:layout_constraintStart_toEndOf="@+id/num"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background android:drawable="@color/ic_launcher_background" />
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
|
@ -23,9 +23,9 @@ private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') + '_' +
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
fun getRandomString(size: Int = Random.nextInt(1, 255)): String {
|
fun getRandomString(size: Int = Random.nextInt(1, 255)): String {
|
||||||
return (1..size)
|
return (1..size)
|
||||||
.map { Random.nextInt(0, charPool.size) }
|
.map { Random.nextInt(0, charPool.size) }
|
||||||
.map(charPool::get)
|
.map(charPool::get)
|
||||||
.joinToString("")
|
.joinToString("")
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL-save version (RFC 4648)
|
// URL-save version (RFC 4648)
|
||||||
|
@ -34,9 +34,9 @@ private val base64CharPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') //
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
fun getRandomBase64(size: Int = Random.nextInt(1, MAX_KEY_LENGTH_NEXTCLOUD)): String {
|
fun getRandomBase64(size: Int = Random.nextInt(1, MAX_KEY_LENGTH_NEXTCLOUD)): String {
|
||||||
return (1..size)
|
return (1..size)
|
||||||
.map { Random.nextInt(0, base64CharPool.size) }
|
.map { Random.nextInt(0, base64CharPool.size) }
|
||||||
.map(base64CharPool::get)
|
.map(base64CharPool::get)
|
||||||
.joinToString("")
|
.joinToString("")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ByteArray.toHexString(spacer: String = " "): String {
|
fun ByteArray.toHexString(spacer: String = " "): String {
|
||||||
|
|
|
@ -30,9 +30,9 @@ class CryptoImplTest {
|
||||||
|
|
||||||
private val iv = ByteArray(IV_SIZE).apply { Random.nextBytes(this) }
|
private val iv = ByteArray(IV_SIZE).apply { Random.nextBytes(this) }
|
||||||
private val cleartext = ByteArray(Random.nextInt(Short.MAX_VALUE.toInt()))
|
private val cleartext = ByteArray(Random.nextInt(Short.MAX_VALUE.toInt()))
|
||||||
.apply { Random.nextBytes(this) }
|
.apply { Random.nextBytes(this) }
|
||||||
private val ciphertext = ByteArray(Random.nextInt(Short.MAX_VALUE.toInt()))
|
private val ciphertext = ByteArray(Random.nextInt(Short.MAX_VALUE.toInt()))
|
||||||
.apply { Random.nextBytes(this) }
|
.apply { Random.nextBytes(this) }
|
||||||
private val outputStream = ByteArrayOutputStream()
|
private val outputStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -47,11 +47,16 @@ class CryptoTest {
|
||||||
private val iv = getRandomByteArray(IV_SIZE)
|
private val iv = getRandomByteArray(IV_SIZE)
|
||||||
private val cleartext = getRandomByteArray(Random.nextInt(MAX_SEGMENT_LENGTH))
|
private val cleartext = getRandomByteArray(Random.nextInt(MAX_SEGMENT_LENGTH))
|
||||||
private val ciphertext = getRandomByteArray(Random.nextInt(MAX_SEGMENT_LENGTH))
|
private val ciphertext = getRandomByteArray(Random.nextInt(MAX_SEGMENT_LENGTH))
|
||||||
private val versionHeader = VersionHeader(VERSION, getRandomString(MAX_PACKAGE_LENGTH_SIZE), getRandomString(MAX_KEY_LENGTH_SIZE))
|
private val versionHeader = VersionHeader(
|
||||||
|
VERSION,
|
||||||
|
getRandomString(MAX_PACKAGE_LENGTH_SIZE),
|
||||||
|
getRandomString(MAX_KEY_LENGTH_SIZE)
|
||||||
|
)
|
||||||
private val versionCiphertext = getRandomByteArray(MAX_VERSION_HEADER_SIZE)
|
private val versionCiphertext = getRandomByteArray(MAX_VERSION_HEADER_SIZE)
|
||||||
private val versionSegmentHeader = SegmentHeader(versionCiphertext.size.toShort(), iv)
|
private val versionSegmentHeader = SegmentHeader(versionCiphertext.size.toShort(), iv)
|
||||||
private val outputStream = ByteArrayOutputStream()
|
private val outputStream = ByteArrayOutputStream()
|
||||||
private val segmentHeader = SegmentHeader(ciphertext.size.toShort(), iv)
|
private val segmentHeader = SegmentHeader(ciphertext.size.toShort(), iv)
|
||||||
|
|
||||||
// the headerReader will not actually read the header, so only insert cipher text
|
// the headerReader will not actually read the header, so only insert cipher text
|
||||||
private val inputStream = ByteArrayInputStream(ciphertext)
|
private val inputStream = ByteArrayInputStream(ciphertext)
|
||||||
private val versionInputStream = ByteArrayInputStream(versionCiphertext)
|
private val versionInputStream = ByteArrayInputStream(versionCiphertext)
|
||||||
|
@ -81,7 +86,10 @@ class CryptoTest {
|
||||||
assertEquals(ciphertext.size, segmentHeader.captured.segmentLength.toInt())
|
assertEquals(ciphertext.size, segmentHeader.captured.segmentLength.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun encryptSegmentHeader(toEncrypt: ByteArray, segmentHeader: CapturingSlot<SegmentHeader>) {
|
private fun encryptSegmentHeader(
|
||||||
|
toEncrypt: ByteArray,
|
||||||
|
segmentHeader: CapturingSlot<SegmentHeader>
|
||||||
|
) {
|
||||||
every { cipherFactory.createEncryptionCipher() } returns cipher
|
every { cipherFactory.createEncryptionCipher() } returns cipher
|
||||||
every { cipher.getOutputSize(toEncrypt.size) } returns toEncrypt.size
|
every { cipher.getOutputSize(toEncrypt.size) } returns toEncrypt.size
|
||||||
every { cipher.iv } returns iv
|
every { cipher.iv } returns iv
|
||||||
|
@ -99,8 +107,13 @@ class CryptoTest {
|
||||||
every { headerReader.getVersionHeader(cleartext) } returns versionHeader
|
every { headerReader.getVersionHeader(cleartext) } returns versionHeader
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
versionHeader,
|
versionHeader,
|
||||||
crypto.decryptHeader(versionInputStream, versionHeader.version, versionHeader.packageName, versionHeader.key)
|
crypto.decryptHeader(
|
||||||
|
versionInputStream,
|
||||||
|
versionHeader.version,
|
||||||
|
versionHeader.packageName,
|
||||||
|
versionHeader.key
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +127,12 @@ class CryptoTest {
|
||||||
every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
|
every { headerReader.readSegmentHeader(versionInputStream) } returns versionSegmentHeader
|
||||||
|
|
||||||
val e = assertThrows(SecurityException::class.java) {
|
val e = assertThrows(SecurityException::class.java) {
|
||||||
crypto.decryptHeader(versionInputStream, versionHeader.version, versionHeader.packageName, versionHeader.key)
|
crypto.decryptHeader(
|
||||||
|
versionInputStream,
|
||||||
|
versionHeader.version,
|
||||||
|
versionHeader.packageName,
|
||||||
|
versionHeader.key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
assertContains(e.message, size.toString())
|
assertContains(e.message, size.toString())
|
||||||
}
|
}
|
||||||
|
@ -128,7 +146,12 @@ class CryptoTest {
|
||||||
|
|
||||||
val version = (VERSION + 1).toByte()
|
val version = (VERSION + 1).toByte()
|
||||||
val e = assertThrows(SecurityException::class.java) {
|
val e = assertThrows(SecurityException::class.java) {
|
||||||
crypto.decryptHeader(versionInputStream, version, versionHeader.packageName, versionHeader.key)
|
crypto.decryptHeader(
|
||||||
|
versionInputStream,
|
||||||
|
version,
|
||||||
|
versionHeader.packageName,
|
||||||
|
versionHeader.key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
assertContains(e.message, version.toString())
|
assertContains(e.message, version.toString())
|
||||||
}
|
}
|
||||||
|
@ -142,7 +165,12 @@ class CryptoTest {
|
||||||
|
|
||||||
val packageName = getRandomString(MAX_PACKAGE_LENGTH_SIZE)
|
val packageName = getRandomString(MAX_PACKAGE_LENGTH_SIZE)
|
||||||
val e = assertThrows(SecurityException::class.java) {
|
val e = assertThrows(SecurityException::class.java) {
|
||||||
crypto.decryptHeader(versionInputStream, versionHeader.version, packageName, versionHeader.key)
|
crypto.decryptHeader(
|
||||||
|
versionInputStream,
|
||||||
|
versionHeader.version,
|
||||||
|
packageName,
|
||||||
|
versionHeader.key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
assertContains(e.message, packageName)
|
assertContains(e.message, packageName)
|
||||||
}
|
}
|
||||||
|
@ -155,7 +183,12 @@ class CryptoTest {
|
||||||
every { headerReader.getVersionHeader(cleartext) } returns versionHeader
|
every { headerReader.getVersionHeader(cleartext) } returns versionHeader
|
||||||
|
|
||||||
val e = assertThrows(SecurityException::class.java) {
|
val e = assertThrows(SecurityException::class.java) {
|
||||||
crypto.decryptHeader(versionInputStream, versionHeader.version, versionHeader.packageName, null)
|
crypto.decryptHeader(
|
||||||
|
versionInputStream,
|
||||||
|
versionHeader.version,
|
||||||
|
versionHeader.packageName,
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
assertContains(e.message, "null")
|
assertContains(e.message, "null")
|
||||||
assertContains(e.message, versionHeader.key ?: fail())
|
assertContains(e.message, versionHeader.key ?: fail())
|
||||||
|
|
|
@ -97,10 +97,10 @@ internal class HeaderReaderTest {
|
||||||
fun `too large package length in VersionHeader throws`() {
|
fun `too large package length in VersionHeader throws`() {
|
||||||
val size = MAX_PACKAGE_LENGTH_SIZE + 1
|
val size = MAX_PACKAGE_LENGTH_SIZE + 1
|
||||||
val input = ByteBuffer.allocate(3 + size)
|
val input = ByteBuffer.allocate(3 + size)
|
||||||
.put(VERSION)
|
.put(VERSION)
|
||||||
.putShort(size.toShort())
|
.putShort(size.toShort())
|
||||||
.put(ByteArray(size))
|
.put(ByteArray(size))
|
||||||
.array()
|
.array()
|
||||||
val e = assertThrows(SecurityException::class.javaObjectType) {
|
val e = assertThrows(SecurityException::class.javaObjectType) {
|
||||||
reader.getVersionHeader(input)
|
reader.getVersionHeader(input)
|
||||||
}
|
}
|
||||||
|
@ -137,11 +137,11 @@ internal class HeaderReaderTest {
|
||||||
fun `too large key length in VersionHeader throws`() {
|
fun `too large key length in VersionHeader throws`() {
|
||||||
val size = MAX_KEY_LENGTH_SIZE + 1
|
val size = MAX_KEY_LENGTH_SIZE + 1
|
||||||
val input = ByteBuffer.allocate(4 + size)
|
val input = ByteBuffer.allocate(4 + size)
|
||||||
.put(VERSION)
|
.put(VERSION)
|
||||||
.putShort(1.toShort())
|
.putShort(1.toShort())
|
||||||
.put("a".toByteArray(Utf8))
|
.put("a".toByteArray(Utf8))
|
||||||
.putShort(size.toShort())
|
.putShort(size.toShort())
|
||||||
.array()
|
.array()
|
||||||
val e = assertThrows(SecurityException::class.javaObjectType) {
|
val e = assertThrows(SecurityException::class.javaObjectType) {
|
||||||
reader.getVersionHeader(input)
|
reader.getVersionHeader(input)
|
||||||
}
|
}
|
||||||
|
@ -171,12 +171,12 @@ internal class HeaderReaderTest {
|
||||||
val packageName = getRandomString(MAX_PACKAGE_LENGTH_SIZE)
|
val packageName = getRandomString(MAX_PACKAGE_LENGTH_SIZE)
|
||||||
val key = getRandomString(MAX_KEY_LENGTH_SIZE)
|
val key = getRandomString(MAX_KEY_LENGTH_SIZE)
|
||||||
val input = ByteBuffer.allocate(MAX_VERSION_HEADER_SIZE)
|
val input = ByteBuffer.allocate(MAX_VERSION_HEADER_SIZE)
|
||||||
.put(VERSION)
|
.put(VERSION)
|
||||||
.putShort(MAX_PACKAGE_LENGTH_SIZE.toShort())
|
.putShort(MAX_PACKAGE_LENGTH_SIZE.toShort())
|
||||||
.put(packageName.toByteArray(Utf8))
|
.put(packageName.toByteArray(Utf8))
|
||||||
.putShort(MAX_KEY_LENGTH_SIZE.toShort())
|
.putShort(MAX_KEY_LENGTH_SIZE.toShort())
|
||||||
.put(key.toByteArray(Utf8))
|
.put(key.toByteArray(Utf8))
|
||||||
.array()
|
.array()
|
||||||
assertEquals(MAX_VERSION_HEADER_SIZE, input.size)
|
assertEquals(MAX_VERSION_HEADER_SIZE, input.size)
|
||||||
val h = reader.getVersionHeader(input)
|
val h = reader.getVersionHeader(input)
|
||||||
assertEquals(VERSION, h.version)
|
assertEquals(VERSION, h.version)
|
||||||
|
@ -188,7 +188,10 @@ internal class HeaderReaderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `too short SegmentHeader throws exception`() {
|
fun `too short SegmentHeader throws exception`() {
|
||||||
val input = byteArrayOf(0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
val input = byteArrayOf(
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertThrows(IOException::class.javaObjectType) {
|
assertThrows(IOException::class.javaObjectType) {
|
||||||
reader.readSegmentHeader(inputStream)
|
reader.readSegmentHeader(inputStream)
|
||||||
|
@ -197,7 +200,10 @@ internal class HeaderReaderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `segment length of zero is rejected`() {
|
fun `segment length of zero is rejected`() {
|
||||||
val input = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
val input = byteArrayOf(
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertThrows(IOException::class.javaObjectType) {
|
assertThrows(IOException::class.javaObjectType) {
|
||||||
reader.readSegmentHeader(inputStream)
|
reader.readSegmentHeader(inputStream)
|
||||||
|
@ -206,7 +212,10 @@ internal class HeaderReaderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `negative segment length is rejected`() {
|
fun `negative segment length is rejected`() {
|
||||||
val input = byteArrayOf(0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
val input = byteArrayOf(
|
||||||
|
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertThrows(IOException::class.javaObjectType) {
|
assertThrows(IOException::class.javaObjectType) {
|
||||||
reader.readSegmentHeader(inputStream)
|
reader.readSegmentHeader(inputStream)
|
||||||
|
@ -215,7 +224,10 @@ internal class HeaderReaderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `minimum negative segment length is rejected`() {
|
fun `minimum negative segment length is rejected`() {
|
||||||
val input = byteArrayOf(0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
val input = byteArrayOf(
|
||||||
|
0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertThrows(IOException::class.javaObjectType) {
|
assertThrows(IOException::class.javaObjectType) {
|
||||||
reader.readSegmentHeader(inputStream)
|
reader.readSegmentHeader(inputStream)
|
||||||
|
@ -224,14 +236,23 @@ internal class HeaderReaderTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `max segment length is accepted`() {
|
fun `max segment length is accepted`() {
|
||||||
val input = byteArrayOf(0x7F, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
val input = byteArrayOf(
|
||||||
|
0x7F, 0xFF, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertEquals(MAX_SEGMENT_LENGTH, reader.readSegmentHeader(inputStream).segmentLength.toInt())
|
assertEquals(
|
||||||
|
MAX_SEGMENT_LENGTH,
|
||||||
|
reader.readSegmentHeader(inputStream).segmentLength.toInt()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `min segment length of 1 is accepted`() {
|
fun `min segment length of 1 is accepted`() {
|
||||||
val input = byteArrayOf(0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
val input = byteArrayOf(
|
||||||
|
0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertEquals(1, reader.readSegmentHeader(inputStream).segmentLength.toInt())
|
assertEquals(1, reader.readSegmentHeader(inputStream).segmentLength.toInt())
|
||||||
}
|
}
|
||||||
|
@ -240,17 +261,23 @@ internal class HeaderReaderTest {
|
||||||
fun `segment length is always read correctly`() {
|
fun `segment length is always read correctly`() {
|
||||||
val segmentLength = getRandomValidSegmentLength()
|
val segmentLength = getRandomValidSegmentLength()
|
||||||
val input = ByteBuffer.allocate(SEGMENT_HEADER_SIZE)
|
val input = ByteBuffer.allocate(SEGMENT_HEADER_SIZE)
|
||||||
.putShort(segmentLength)
|
.putShort(segmentLength)
|
||||||
.put(ByteArray(IV_SIZE))
|
.put(ByteArray(IV_SIZE))
|
||||||
.array()
|
.array()
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertEquals(segmentLength, reader.readSegmentHeader(inputStream).segmentLength)
|
assertEquals(segmentLength, reader.readSegmentHeader(inputStream).segmentLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `nonce is read in big endian`() {
|
fun `nonce is read in big endian`() {
|
||||||
val nonce = byteArrayOf(0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01)
|
val nonce = byteArrayOf(
|
||||||
val input = byteArrayOf(0x00, 0x01, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01)
|
0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x01
|
||||||
|
)
|
||||||
|
val input = byteArrayOf(
|
||||||
|
0x00, 0x01, 0xff, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
|
||||||
|
)
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertArrayEquals(nonce, reader.readSegmentHeader(inputStream).nonce)
|
assertArrayEquals(nonce, reader.readSegmentHeader(inputStream).nonce)
|
||||||
}
|
}
|
||||||
|
@ -259,9 +286,9 @@ internal class HeaderReaderTest {
|
||||||
fun `nonce is always read correctly`() {
|
fun `nonce is always read correctly`() {
|
||||||
val nonce = ByteArray(IV_SIZE).apply { Random.nextBytes(this) }
|
val nonce = ByteArray(IV_SIZE).apply { Random.nextBytes(this) }
|
||||||
val input = ByteBuffer.allocate(SEGMENT_HEADER_SIZE)
|
val input = ByteBuffer.allocate(SEGMENT_HEADER_SIZE)
|
||||||
.putShort(1)
|
.putShort(1)
|
||||||
.put(nonce)
|
.put(nonce)
|
||||||
.array()
|
.array()
|
||||||
val inputStream = ByteArrayInputStream(input)
|
val inputStream = ByteArrayInputStream(input)
|
||||||
assertArrayEquals(nonce, reader.readSegmentHeader(inputStream).nonce)
|
assertArrayEquals(nonce, reader.readSegmentHeader(inputStream).nonce)
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,10 +79,10 @@ class MetadataManagerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test onApkBackedUp() with no prior package metadata`() {
|
fun `test onApkBackedUp() with no prior package metadata`() {
|
||||||
val packageMetadata = PackageMetadata(
|
val packageMetadata = PackageMetadata(
|
||||||
time = 0L,
|
time = 0L,
|
||||||
version = Random.nextLong(Long.MAX_VALUE),
|
version = Random.nextLong(Long.MAX_VALUE),
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
signatures = listOf("sig")
|
signatures = listOf("sig")
|
||||||
)
|
)
|
||||||
|
|
||||||
expectReadFromCache()
|
expectReadFromCache()
|
||||||
|
@ -97,10 +97,10 @@ class MetadataManagerTest {
|
||||||
fun `test onApkBackedUp() sets system metadata`() {
|
fun `test onApkBackedUp() sets system metadata`() {
|
||||||
packageInfo.applicationInfo = ApplicationInfo().apply { flags = FLAG_SYSTEM }
|
packageInfo.applicationInfo = ApplicationInfo().apply { flags = FLAG_SYSTEM }
|
||||||
val packageMetadata = PackageMetadata(
|
val packageMetadata = PackageMetadata(
|
||||||
time = 0L,
|
time = 0L,
|
||||||
version = Random.nextLong(Long.MAX_VALUE),
|
version = Random.nextLong(Long.MAX_VALUE),
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
signatures = listOf("sig")
|
signatures = listOf("sig")
|
||||||
)
|
)
|
||||||
|
|
||||||
expectReadFromCache()
|
expectReadFromCache()
|
||||||
|
@ -114,17 +114,17 @@ class MetadataManagerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test onApkBackedUp() with existing package metadata`() {
|
fun `test onApkBackedUp() with existing package metadata`() {
|
||||||
val packageMetadata = PackageMetadata(
|
val packageMetadata = PackageMetadata(
|
||||||
time = time,
|
time = time,
|
||||||
version = Random.nextLong(Long.MAX_VALUE),
|
version = Random.nextLong(Long.MAX_VALUE),
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
signatures = listOf("sig")
|
signatures = listOf("sig")
|
||||||
)
|
)
|
||||||
initialMetadata.packageMetadataMap[packageName] = packageMetadata
|
initialMetadata.packageMetadataMap[packageName] = packageMetadata
|
||||||
val updatedPackageMetadata = PackageMetadata(
|
val updatedPackageMetadata = PackageMetadata(
|
||||||
time = time,
|
time = time,
|
||||||
version = packageMetadata.version!! + 1,
|
version = packageMetadata.version!! + 1,
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
signatures = listOf("sig foo")
|
signatures = listOf("sig foo")
|
||||||
)
|
)
|
||||||
|
|
||||||
expectReadFromCache()
|
expectReadFromCache()
|
||||||
|
@ -139,9 +139,9 @@ class MetadataManagerTest {
|
||||||
fun `test onApkBackedUp() limits state changes`() {
|
fun `test onApkBackedUp() limits state changes`() {
|
||||||
var version = Random.nextLong(Long.MAX_VALUE)
|
var version = Random.nextLong(Long.MAX_VALUE)
|
||||||
var packageMetadata = PackageMetadata(
|
var packageMetadata = PackageMetadata(
|
||||||
version = version,
|
version = version,
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
signatures = listOf("sig")
|
signatures = listOf("sig")
|
||||||
)
|
)
|
||||||
|
|
||||||
expectReadFromCache()
|
expectReadFromCache()
|
||||||
|
@ -151,35 +151,50 @@ class MetadataManagerTest {
|
||||||
// state doesn't change for APK_AND_DATA
|
// state doesn't change for APK_AND_DATA
|
||||||
packageMetadata = packageMetadata.copy(version = ++version, state = APK_AND_DATA)
|
packageMetadata = packageMetadata.copy(version = ++version, state = APK_AND_DATA)
|
||||||
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
||||||
assertEquals(packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
packageMetadata.copy(state = oldState),
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
|
|
||||||
// state doesn't change for QUOTA_EXCEEDED
|
// state doesn't change for QUOTA_EXCEEDED
|
||||||
packageMetadata = packageMetadata.copy(version = ++version, state = QUOTA_EXCEEDED)
|
packageMetadata = packageMetadata.copy(version = ++version, state = QUOTA_EXCEEDED)
|
||||||
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
||||||
assertEquals(packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
packageMetadata.copy(state = oldState),
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
|
|
||||||
// state doesn't change for NO_DATA
|
// state doesn't change for NO_DATA
|
||||||
packageMetadata = packageMetadata.copy(version = ++version, state = NO_DATA)
|
packageMetadata = packageMetadata.copy(version = ++version, state = NO_DATA)
|
||||||
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
||||||
assertEquals(packageMetadata.copy(state = oldState), manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
packageMetadata.copy(state = oldState),
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
|
|
||||||
// state DOES change for NOT_ALLOWED
|
// state DOES change for NOT_ALLOWED
|
||||||
packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED)
|
packageMetadata = packageMetadata.copy(version = ++version, state = NOT_ALLOWED)
|
||||||
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
||||||
assertEquals(packageMetadata.copy(state = NOT_ALLOWED), manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
packageMetadata.copy(state = NOT_ALLOWED),
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
|
|
||||||
// state DOES change for WAS_STOPPED
|
// state DOES change for WAS_STOPPED
|
||||||
packageMetadata = packageMetadata.copy(version = ++version, state = WAS_STOPPED)
|
packageMetadata = packageMetadata.copy(version = ++version, state = WAS_STOPPED)
|
||||||
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
manager.onApkBackedUp(packageInfo, packageMetadata, storageOutputStream)
|
||||||
assertEquals(packageMetadata.copy(state = WAS_STOPPED), manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
packageMetadata.copy(state = WAS_STOPPED),
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test onPackageBackedUp()`() {
|
fun `test onPackageBackedUp()`() {
|
||||||
packageInfo.applicationInfo.flags = FLAG_SYSTEM
|
packageInfo.applicationInfo.flags = FLAG_SYSTEM
|
||||||
val updatedMetadata = initialMetadata.copy(
|
val updatedMetadata = initialMetadata.copy(
|
||||||
time = time,
|
time = time,
|
||||||
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
|
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
|
||||||
)
|
)
|
||||||
val packageMetadata = PackageMetadata(time)
|
val packageMetadata = PackageMetadata(time)
|
||||||
updatedMetadata.packageMetadataMap[packageName] = packageMetadata
|
updatedMetadata.packageMetadataMap[packageName] = packageMetadata
|
||||||
|
@ -190,7 +205,10 @@ class MetadataManagerTest {
|
||||||
|
|
||||||
manager.onPackageBackedUp(packageInfo, storageOutputStream)
|
manager.onPackageBackedUp(packageInfo, storageOutputStream)
|
||||||
|
|
||||||
assertEquals(packageMetadata.copy(state = APK_AND_DATA, system = true), manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
packageMetadata.copy(state = APK_AND_DATA, system = true),
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
assertEquals(time, manager.getLastBackupTime())
|
assertEquals(time, manager.getLastBackupTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,8 +216,8 @@ class MetadataManagerTest {
|
||||||
fun `test onPackageBackedUp() fails to write to storage`() {
|
fun `test onPackageBackedUp() fails to write to storage`() {
|
||||||
val updateTime = time + 1
|
val updateTime = time + 1
|
||||||
val updatedMetadata = initialMetadata.copy(
|
val updatedMetadata = initialMetadata.copy(
|
||||||
time = updateTime,
|
time = updateTime,
|
||||||
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
|
packageMetadataMap = PackageMetadataMap() // otherwise this isn't copied, but referenced
|
||||||
)
|
)
|
||||||
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(updateTime, APK_AND_DATA)
|
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(updateTime, APK_AND_DATA)
|
||||||
|
|
||||||
|
@ -215,7 +233,10 @@ class MetadataManagerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(0L, manager.getLastBackupTime()) // time was reverted
|
assertEquals(0L, manager.getLastBackupTime()) // time was reverted
|
||||||
assertEquals(initialMetadata.packageMetadataMap[packageName], manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
initialMetadata.packageMetadataMap[packageName],
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -229,7 +250,8 @@ class MetadataManagerTest {
|
||||||
|
|
||||||
val updatedMetadata = cachedMetadata.copy(time = time)
|
val updatedMetadata = cachedMetadata.copy(time = time)
|
||||||
updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
|
updatedMetadata.packageMetadataMap[cachedPackageName] = PackageMetadata(time)
|
||||||
updatedMetadata.packageMetadataMap[packageName] = PackageMetadata(time, state = APK_AND_DATA)
|
updatedMetadata.packageMetadataMap[packageName] =
|
||||||
|
PackageMetadata(time, state = APK_AND_DATA)
|
||||||
|
|
||||||
expectReadFromCache()
|
expectReadFromCache()
|
||||||
every { clock.time() } returns time
|
every { clock.time() } returns time
|
||||||
|
@ -239,7 +261,10 @@ class MetadataManagerTest {
|
||||||
|
|
||||||
assertEquals(time, manager.getLastBackupTime())
|
assertEquals(time, manager.getLastBackupTime())
|
||||||
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
|
assertEquals(PackageMetadata(time), manager.getPackageMetadata(cachedPackageName))
|
||||||
assertEquals(updatedMetadata.packageMetadataMap[packageName], manager.getPackageMetadata(packageName))
|
assertEquals(
|
||||||
|
updatedMetadata.packageMetadataMap[packageName],
|
||||||
|
manager.getPackageMetadata(packageName)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -269,7 +294,12 @@ class MetadataManagerTest {
|
||||||
private fun expectModifyMetadata(metadata: BackupMetadata) {
|
private fun expectModifyMetadata(metadata: BackupMetadata) {
|
||||||
every { metadataWriter.write(metadata, storageOutputStream) } just Runs
|
every { metadataWriter.write(metadata, storageOutputStream) } just Runs
|
||||||
every { metadataWriter.encode(metadata) } returns encodedMetadata
|
every { metadataWriter.encode(metadata) } returns encodedMetadata
|
||||||
every { context.openFileOutput(METADATA_CACHE_FILE, MODE_PRIVATE) } returns cacheOutputStream
|
every {
|
||||||
|
context.openFileOutput(
|
||||||
|
METADATA_CACHE_FILE,
|
||||||
|
MODE_PRIVATE
|
||||||
|
)
|
||||||
|
} returns cacheOutputStream
|
||||||
every { cacheOutputStream.write(encodedMetadata) } just Runs
|
every { cacheOutputStream.write(encodedMetadata) } just Runs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,14 +86,16 @@ class MetadataReaderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `package metadata gets read`() {
|
fun `package metadata gets read`() {
|
||||||
val packageMetadata = HashMap<String, PackageMetadata>().apply {
|
val packageMetadata = HashMap<String, PackageMetadata>().apply {
|
||||||
put("org.example", PackageMetadata(
|
put(
|
||||||
|
"org.example", PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
state = QUOTA_EXCEEDED,
|
state = QUOTA_EXCEEDED,
|
||||||
version = Random.nextLong(),
|
version = Random.nextLong(),
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
sha256 = getRandomString(),
|
sha256 = getRandomString(),
|
||||||
signatures = listOf(getRandomString(), getRandomString())
|
signatures = listOf(getRandomString(), getRandomString())
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val metadata = getMetadata(packageMetadata)
|
val metadata = getMetadata(packageMetadata)
|
||||||
val metadataByteArray = encoder.encode(metadata)
|
val metadataByteArray = encoder.encode(metadata)
|
||||||
|
@ -160,13 +162,13 @@ class MetadataReaderTest {
|
||||||
|
|
||||||
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(),
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
androidVersion = Random.nextInt(),
|
androidVersion = Random.nextInt(),
|
||||||
androidIncremental = getRandomString(),
|
androidIncremental = getRandomString(),
|
||||||
deviceName = getRandomString(),
|
deviceName = getRandomString(),
|
||||||
packageMetadataMap = packageMetadata
|
packageMetadataMap = packageMetadata
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,10 @@ internal class MetadataWriterDecoderTest {
|
||||||
@Test
|
@Test
|
||||||
fun `encoded metadata matches decoded metadata (no packages)`() {
|
fun `encoded metadata matches decoded metadata (no packages)`() {
|
||||||
val metadata = getMetadata()
|
val metadata = getMetadata()
|
||||||
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
assertEquals(
|
||||||
|
metadata,
|
||||||
|
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -36,28 +39,38 @@ internal class MetadataWriterDecoderTest {
|
||||||
put(getRandomString(), PackageMetadata(time, WAS_STOPPED))
|
put(getRandomString(), PackageMetadata(time, WAS_STOPPED))
|
||||||
}
|
}
|
||||||
val metadata = getMetadata(packages)
|
val metadata = getMetadata(packages)
|
||||||
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
assertEquals(
|
||||||
|
metadata,
|
||||||
|
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `encoded metadata matches decoded metadata (full package)`() {
|
fun `encoded metadata matches decoded metadata (full package)`() {
|
||||||
val packages = HashMap<String, PackageMetadata>().apply {
|
val packages = HashMap<String, PackageMetadata>().apply {
|
||||||
put(getRandomString(), PackageMetadata(
|
put(
|
||||||
|
getRandomString(), PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
state = APK_AND_DATA,
|
state = APK_AND_DATA,
|
||||||
version = Random.nextLong(),
|
version = Random.nextLong(),
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
sha256 = getRandomString(),
|
sha256 = getRandomString(),
|
||||||
signatures = listOf(getRandomString(), getRandomString())))
|
signatures = listOf(getRandomString(), getRandomString())
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val metadata = getMetadata(packages)
|
val metadata = getMetadata(packages)
|
||||||
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
assertEquals(
|
||||||
|
metadata,
|
||||||
|
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `encoded metadata matches decoded metadata (three full packages)`() {
|
fun `encoded metadata matches decoded metadata (three full packages)`() {
|
||||||
val packages = HashMap<String, PackageMetadata>().apply {
|
val packages = HashMap<String, PackageMetadata>().apply {
|
||||||
put(getRandomString(), PackageMetadata(
|
put(
|
||||||
|
getRandomString(), PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
state = QUOTA_EXCEEDED,
|
state = QUOTA_EXCEEDED,
|
||||||
system = Random.nextBoolean(),
|
system = Random.nextBoolean(),
|
||||||
|
@ -65,8 +78,10 @@ internal class MetadataWriterDecoderTest {
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
sha256 = getRandomString(),
|
sha256 = getRandomString(),
|
||||||
signatures = listOf(getRandomString())
|
signatures = listOf(getRandomString())
|
||||||
))
|
)
|
||||||
put(getRandomString(), PackageMetadata(
|
)
|
||||||
|
put(
|
||||||
|
getRandomString(), PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
state = NO_DATA,
|
state = NO_DATA,
|
||||||
system = Random.nextBoolean(),
|
system = Random.nextBoolean(),
|
||||||
|
@ -74,8 +89,10 @@ internal class MetadataWriterDecoderTest {
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
sha256 = getRandomString(),
|
sha256 = getRandomString(),
|
||||||
signatures = listOf(getRandomString(), getRandomString())
|
signatures = listOf(getRandomString(), getRandomString())
|
||||||
))
|
)
|
||||||
put(getRandomString(), PackageMetadata(
|
)
|
||||||
|
put(
|
||||||
|
getRandomString(), PackageMetadata(
|
||||||
time = 0L,
|
time = 0L,
|
||||||
state = NOT_ALLOWED,
|
state = NOT_ALLOWED,
|
||||||
system = Random.nextBoolean(),
|
system = Random.nextBoolean(),
|
||||||
|
@ -83,21 +100,25 @@ internal class MetadataWriterDecoderTest {
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
sha256 = getRandomString(),
|
sha256 = getRandomString(),
|
||||||
signatures = listOf(getRandomString(), getRandomString())
|
signatures = listOf(getRandomString(), getRandomString())
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val metadata = getMetadata(packages)
|
val metadata = getMetadata(packages)
|
||||||
assertEquals(metadata, decoder.decode(encoder.encode(metadata), metadata.version, metadata.token))
|
assertEquals(
|
||||||
|
metadata,
|
||||||
|
decoder.decode(encoder.encode(metadata), metadata.version, metadata.token)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
androidVersion = Random.nextInt(),
|
androidVersion = Random.nextInt(),
|
||||||
androidIncremental = getRandomString(),
|
androidIncremental = getRandomString(),
|
||||||
deviceName = getRandomString(),
|
deviceName = getRandomString(),
|
||||||
packageMetadataMap = packageMetadata
|
packageMetadataMap = packageMetadata
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,12 +21,12 @@ internal class DocumentFileTest {
|
||||||
private val context: Context = mockk()
|
private val context: Context = mockk()
|
||||||
private val parentUri: Uri = Uri.parse(
|
private val parentUri: Uri = Uri.parse(
|
||||||
"content://com.android.externalstorage.documents/tree/" +
|
"content://com.android.externalstorage.documents/tree/" +
|
||||||
"primary%3A/document/primary%3A.SeedVaultAndroidBackup"
|
"primary%3A/document/primary%3A.SeedVaultAndroidBackup"
|
||||||
)
|
)
|
||||||
private val parentFile: DocumentFile = DocumentFile.fromTreeUri(context, parentUri)!!
|
private val parentFile: DocumentFile = DocumentFile.fromTreeUri(context, parentUri)!!
|
||||||
private val uri: Uri = Uri.parse(
|
private val uri: Uri = Uri.parse(
|
||||||
"content://com.android.externalstorage.documents/tree/" +
|
"content://com.android.externalstorage.documents/tree/" +
|
||||||
"primary%3A/document/primary%3A.SeedVaultAndroidBackup%2Ftest"
|
"primary%3A/document/primary%3A.SeedVaultAndroidBackup%2Ftest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|
|
@ -65,7 +65,8 @@ internal class CoordinatorIntegrationTest : TransportTest() {
|
||||||
|
|
||||||
private val backupPlugin = mockk<BackupPlugin>()
|
private val backupPlugin = mockk<BackupPlugin>()
|
||||||
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
private val kvBackupPlugin = mockk<KVBackupPlugin>()
|
||||||
private val kvBackup = KVBackup(kvBackupPlugin, inputFactory, headerWriter, cryptoImpl, notificationManager)
|
private val kvBackup =
|
||||||
|
KVBackup(kvBackupPlugin, inputFactory, headerWriter, cryptoImpl, notificationManager)
|
||||||
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
private val fullBackupPlugin = mockk<FullBackupPlugin>()
|
||||||
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
private val fullBackup = FullBackup(fullBackupPlugin, inputFactory, headerWriter, cryptoImpl)
|
||||||
private val apkBackup = mockk<ApkBackup>()
|
private val apkBackup = mockk<ApkBackup>()
|
||||||
|
|
|
@ -162,21 +162,22 @@ internal class FullBackupTest : BackupTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendBackupData throws exception when writing encrypted data to OutputStream`() = runBlocking {
|
fun `sendBackupData throws exception when writing encrypted data to OutputStream`() =
|
||||||
every { inputFactory.getInputStream(data) } returns inputStream
|
runBlocking {
|
||||||
expectInitializeOutputStream()
|
every { inputFactory.getInputStream(data) } returns inputStream
|
||||||
every { plugin.getQuota() } returns quota
|
expectInitializeOutputStream()
|
||||||
every { inputStream.read(any(), any(), bytes.size) } returns bytes.size
|
every { plugin.getQuota() } returns quota
|
||||||
every { crypto.encryptSegment(outputStream, any()) } throws IOException()
|
every { inputStream.read(any(), any(), bytes.size) } returns bytes.size
|
||||||
expectClearState()
|
every { crypto.encryptSegment(outputStream, any()) } throws IOException()
|
||||||
|
expectClearState()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data))
|
assertEquals(TRANSPORT_OK, backup.performFullBackup(packageInfo, data))
|
||||||
assertTrue(backup.hasState())
|
assertTrue(backup.hasState())
|
||||||
assertEquals(TRANSPORT_ERROR, backup.sendBackupData(bytes.size))
|
assertEquals(TRANSPORT_ERROR, backup.sendBackupData(bytes.size))
|
||||||
assertTrue(backup.hasState())
|
assertTrue(backup.hasState())
|
||||||
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
assertEquals(TRANSPORT_OK, backup.finishBackup())
|
||||||
assertFalse(backup.hasState())
|
assertFalse(backup.hasState())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `sendBackupData runs ok`() = runBlocking {
|
fun `sendBackupData runs ok`() = runBlocking {
|
||||||
|
|
|
@ -49,11 +49,11 @@ internal class ApkRestoreTest : RestoreTest() {
|
||||||
|
|
||||||
private val packageName = packageInfo.packageName
|
private val packageName = packageInfo.packageName
|
||||||
private val packageMetadata = PackageMetadata(
|
private val packageMetadata = PackageMetadata(
|
||||||
time = Random.nextLong(),
|
time = Random.nextLong(),
|
||||||
version = packageInfo.longVersionCode - 1,
|
version = packageInfo.longVersionCode - 1,
|
||||||
installer = getRandomString(),
|
installer = getRandomString(),
|
||||||
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
sha256 = "eHx5jjmlvBkQNVuubQzYejay4Q_QICqD47trAF2oNHI",
|
||||||
signatures = listOf("AwIB")
|
signatures = listOf("AwIB")
|
||||||
)
|
)
|
||||||
private val packageMetadataMap: PackageMetadataMap = hashMapOf(packageName to packageMetadata)
|
private val packageMetadataMap: PackageMetadataMap = hashMapOf(packageName to packageMetadata)
|
||||||
private val apkBytes = byteArrayOf(0x04, 0x05, 0x06)
|
private val apkBytes = byteArrayOf(0x04, 0x05, 0x06)
|
||||||
|
@ -123,9 +123,21 @@ internal class ApkRestoreTest : RestoreTest() {
|
||||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
|
coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
|
||||||
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||||
every { pm.loadItemIcon(packageInfo.applicationInfo, packageInfo.applicationInfo) } returns icon
|
every {
|
||||||
|
pm.loadItemIcon(
|
||||||
|
packageInfo.applicationInfo,
|
||||||
|
packageInfo.applicationInfo
|
||||||
|
)
|
||||||
|
} returns icon
|
||||||
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
||||||
every { apkInstaller.install(any(), packageName, installerName, any()) } throws SecurityException()
|
every {
|
||||||
|
apkInstaller.install(
|
||||||
|
any(),
|
||||||
|
packageName,
|
||||||
|
installerName,
|
||||||
|
any()
|
||||||
|
)
|
||||||
|
} throws SecurityException()
|
||||||
|
|
||||||
apkRestore.restore(token, packageMetadataMap).collectIndexed { index, value ->
|
apkRestore.restore(token, packageMetadataMap).collectIndexed { index, value ->
|
||||||
when (index) {
|
when (index) {
|
||||||
|
@ -153,15 +165,29 @@ internal class ApkRestoreTest : RestoreTest() {
|
||||||
@Test
|
@Test
|
||||||
fun `test successful run`(@TempDir tmpDir: Path) = runBlocking {
|
fun `test successful run`(@TempDir tmpDir: Path) = runBlocking {
|
||||||
val installResult = MutableInstallResult(1).apply {
|
val installResult = MutableInstallResult(1).apply {
|
||||||
put(packageName, ApkRestoreResult(packageName, progress = 1, total = 1, status = SUCCEEDED))
|
put(
|
||||||
|
packageName, ApkRestoreResult(
|
||||||
|
packageName,
|
||||||
|
progress = 1,
|
||||||
|
total = 1,
|
||||||
|
status = SUCCEEDED
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
|
coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
|
||||||
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||||
every { pm.loadItemIcon(packageInfo.applicationInfo, packageInfo.applicationInfo) } returns icon
|
every {
|
||||||
|
pm.loadItemIcon(
|
||||||
|
packageInfo.applicationInfo,
|
||||||
|
packageInfo.applicationInfo
|
||||||
|
)
|
||||||
|
} returns icon
|
||||||
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
||||||
every { apkInstaller.install(any(), packageName, installerName, any()) } returns flowOf(installResult)
|
every { apkInstaller.install(any(), packageName, installerName, any()) } returns flowOf(
|
||||||
|
installResult
|
||||||
|
)
|
||||||
|
|
||||||
var i = 0
|
var i = 0
|
||||||
apkRestore.restore(token, packageMetadataMap).collect { value ->
|
apkRestore.restore(token, packageMetadataMap).collect { value ->
|
||||||
|
@ -189,59 +215,75 @@ internal class ApkRestoreTest : RestoreTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test system apps only get reinstalled when older system apps exist`(@TempDir tmpDir: Path) = runBlocking {
|
fun `test system apps only get reinstalled when older system apps exist`(@TempDir tmpDir: Path) =
|
||||||
val packageMetadata = this@ApkRestoreTest.packageMetadata.copy(system = true)
|
runBlocking {
|
||||||
packageMetadataMap[packageName] = packageMetadata
|
val packageMetadata = this@ApkRestoreTest.packageMetadata.copy(system = true)
|
||||||
packageInfo.applicationInfo = mockk()
|
packageMetadataMap[packageName] = packageMetadata
|
||||||
val installedPackageInfo: PackageInfo = mockk()
|
packageInfo.applicationInfo = mockk()
|
||||||
val willFail = Random.nextBoolean()
|
val installedPackageInfo: PackageInfo = mockk()
|
||||||
installedPackageInfo.applicationInfo = ApplicationInfo().apply {
|
val willFail = Random.nextBoolean()
|
||||||
// will not fail when app really is a system app
|
installedPackageInfo.applicationInfo = ApplicationInfo().apply {
|
||||||
flags = if (willFail) FLAG_INSTALLED else FLAG_SYSTEM or FLAG_UPDATED_SYSTEM_APP
|
// will not fail when app really is a system app
|
||||||
}
|
flags = if (willFail) FLAG_INSTALLED else FLAG_SYSTEM or FLAG_UPDATED_SYSTEM_APP
|
||||||
|
|
||||||
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
|
||||||
coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
|
|
||||||
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
|
||||||
every { pm.loadItemIcon(packageInfo.applicationInfo, packageInfo.applicationInfo) } returns icon
|
|
||||||
every { packageInfo.applicationInfo.loadIcon(pm) } returns icon
|
|
||||||
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
|
||||||
every { pm.getPackageInfo(packageName, 0) } returns installedPackageInfo
|
|
||||||
every { installedPackageInfo.longVersionCode } returns packageMetadata.version!! - 1
|
|
||||||
if (!willFail) {
|
|
||||||
val installResult = MutableInstallResult(1).apply {
|
|
||||||
put(packageName, ApkRestoreResult(packageName, progress = 1, total = 1, status = SUCCEEDED))
|
|
||||||
}
|
}
|
||||||
every { apkInstaller.install(any(), packageName, installerName, any()) } returns flowOf(installResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
var i = 0
|
every { strictContext.cacheDir } returns File(tmpDir.toString())
|
||||||
apkRestore.restore(token, packageMetadataMap).collect { value ->
|
coEvery { restorePlugin.getApkInputStream(token, packageName) } returns apkInputStream
|
||||||
when (i) {
|
every { pm.getPackageArchiveInfo(any(), any()) } returns packageInfo
|
||||||
0 -> {
|
every {
|
||||||
val result = value[packageName] ?: fail()
|
pm.loadItemIcon(
|
||||||
assertEquals(QUEUED, result.status)
|
packageInfo.applicationInfo,
|
||||||
assertEquals(1, result.progress)
|
packageInfo.applicationInfo
|
||||||
assertEquals(1, result.total)
|
)
|
||||||
|
} returns icon
|
||||||
|
every { packageInfo.applicationInfo.loadIcon(pm) } returns icon
|
||||||
|
every { pm.getApplicationLabel(packageInfo.applicationInfo) } returns appName
|
||||||
|
every { pm.getPackageInfo(packageName, 0) } returns installedPackageInfo
|
||||||
|
every { installedPackageInfo.longVersionCode } returns packageMetadata.version!! - 1
|
||||||
|
if (!willFail) {
|
||||||
|
val installResult = MutableInstallResult(1).apply {
|
||||||
|
put(
|
||||||
|
packageName,
|
||||||
|
ApkRestoreResult(packageName, progress = 1, total = 1, status = SUCCEEDED)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
1 -> {
|
every {
|
||||||
val result = value[packageName] ?: fail()
|
apkInstaller.install(
|
||||||
assertEquals(IN_PROGRESS, result.status)
|
any(),
|
||||||
assertEquals(appName, result.name)
|
packageName,
|
||||||
assertEquals(icon, result.icon)
|
installerName,
|
||||||
}
|
any()
|
||||||
2 -> {
|
)
|
||||||
val result = value[packageName] ?: fail()
|
} returns flowOf(installResult)
|
||||||
if (willFail) {
|
}
|
||||||
assertEquals(FAILED, result.status)
|
|
||||||
} else {
|
var i = 0
|
||||||
assertEquals(SUCCEEDED, result.status)
|
apkRestore.restore(token, packageMetadataMap).collect { value ->
|
||||||
|
when (i) {
|
||||||
|
0 -> {
|
||||||
|
val result = value[packageName] ?: fail()
|
||||||
|
assertEquals(QUEUED, result.status)
|
||||||
|
assertEquals(1, result.progress)
|
||||||
|
assertEquals(1, result.total)
|
||||||
}
|
}
|
||||||
|
1 -> {
|
||||||
|
val result = value[packageName] ?: fail()
|
||||||
|
assertEquals(IN_PROGRESS, result.status)
|
||||||
|
assertEquals(appName, result.name)
|
||||||
|
assertEquals(icon, result.icon)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
val result = value[packageName] ?: fail()
|
||||||
|
if (willFail) {
|
||||||
|
assertEquals(FAILED, result.status)
|
||||||
|
} else {
|
||||||
|
assertEquals(SUCCEEDED, result.status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> fail()
|
||||||
}
|
}
|
||||||
else -> fail()
|
i++
|
||||||
}
|
}
|
||||||
i++
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,10 @@ internal class FullRestoreTest : RestoreTest() {
|
||||||
|
|
||||||
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } throws IOException()
|
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } throws IOException()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
assertEquals(
|
||||||
|
TRANSPORT_PACKAGE_REJECTED,
|
||||||
|
restore.getNextFullRestoreDataChunk(fileDescriptor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -78,7 +81,10 @@ internal class FullRestoreTest : RestoreTest() {
|
||||||
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } throws IOException()
|
every { headerReader.readVersion(inputStream) } throws IOException()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
assertEquals(
|
||||||
|
TRANSPORT_PACKAGE_REJECTED,
|
||||||
|
restore.getNextFullRestoreDataChunk(fileDescriptor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -86,9 +92,14 @@ internal class FullRestoreTest : RestoreTest() {
|
||||||
restore.initializeState(token, packageInfo)
|
restore.initializeState(token, packageInfo)
|
||||||
|
|
||||||
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion)
|
every {
|
||||||
|
headerReader.readVersion(inputStream)
|
||||||
|
} throws UnsupportedVersionException(unsupportedVersion)
|
||||||
|
|
||||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
assertEquals(
|
||||||
|
TRANSPORT_PACKAGE_REJECTED,
|
||||||
|
restore.getNextFullRestoreDataChunk(fileDescriptor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -97,21 +108,37 @@ internal class FullRestoreTest : RestoreTest() {
|
||||||
|
|
||||||
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName) } throws IOException()
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName
|
||||||
|
)
|
||||||
|
} throws IOException()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
assertEquals(
|
||||||
|
TRANSPORT_PACKAGE_REJECTED,
|
||||||
|
restore.getNextFullRestoreDataChunk(fileDescriptor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `decrypting version header when getting first chunk throws security exception`() = runBlocking {
|
fun `decrypting version header when getting first chunk throws security exception`() =
|
||||||
restore.initializeState(token, packageInfo)
|
runBlocking {
|
||||||
|
restore.initializeState(token, packageInfo)
|
||||||
|
|
||||||
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName) } throws SecurityException()
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName
|
||||||
|
)
|
||||||
|
} throws SecurityException()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
assertEquals(TRANSPORT_ERROR, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `decrypting segment throws IOException`() = runBlocking {
|
fun `decrypting segment throws IOException`() = runBlocking {
|
||||||
|
@ -123,7 +150,10 @@ internal class FullRestoreTest : RestoreTest() {
|
||||||
every { inputStream.close() } just Runs
|
every { inputStream.close() } just Runs
|
||||||
every { fileDescriptor.close() } just Runs
|
every { fileDescriptor.close() } just Runs
|
||||||
|
|
||||||
assertEquals(TRANSPORT_PACKAGE_REJECTED, restore.getNextFullRestoreDataChunk(fileDescriptor))
|
assertEquals(
|
||||||
|
TRANSPORT_PACKAGE_REJECTED,
|
||||||
|
restore.getNextFullRestoreDataChunk(fileDescriptor)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -171,7 +201,13 @@ internal class FullRestoreTest : RestoreTest() {
|
||||||
private fun initInputStream() {
|
private fun initInputStream() {
|
||||||
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
coEvery { plugin.getInputStreamForPackage(token, packageInfo) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName) } returns versionHeader
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName
|
||||||
|
)
|
||||||
|
} returns versionHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun readAndEncryptInputStream(encryptedBytes: ByteArray) {
|
private fun readAndEncryptInputStream(encryptedBytes: ByteArray) {
|
||||||
|
|
|
@ -67,7 +67,9 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
|
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } throws UnsupportedVersionException(unsupportedVersion)
|
every {
|
||||||
|
headerReader.readVersion(inputStream)
|
||||||
|
} throws UnsupportedVersionException(unsupportedVersion)
|
||||||
streamsGetClosed()
|
streamsGetClosed()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
||||||
|
@ -94,7 +96,14 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} returns versionHeader
|
||||||
every { crypto.decryptMultipleSegments(inputStream) } throws IOException()
|
every { crypto.decryptMultipleSegments(inputStream) } throws IOException()
|
||||||
streamsGetClosed()
|
streamsGetClosed()
|
||||||
|
|
||||||
|
@ -109,7 +118,14 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws IOException()
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} throws IOException()
|
||||||
streamsGetClosed()
|
streamsGetClosed()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
||||||
|
@ -123,7 +139,14 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } throws SecurityException()
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} throws SecurityException()
|
||||||
streamsGetClosed()
|
streamsGetClosed()
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
assertEquals(TRANSPORT_ERROR, restore.getRestoreData(fileDescriptor))
|
||||||
|
@ -137,7 +160,14 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} returns versionHeader
|
||||||
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
||||||
every { output.writeEntityHeader(key, data.size) } throws IOException()
|
every { output.writeEntityHeader(key, data.size) } throws IOException()
|
||||||
streamsGetClosed()
|
streamsGetClosed()
|
||||||
|
@ -153,7 +183,14 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} returns versionHeader
|
||||||
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
||||||
every { output.writeEntityHeader(key, data.size) } returns 42
|
every { output.writeEntityHeader(key, data.size) } returns 42
|
||||||
every { output.writeEntityData(data, data.size) } throws IOException()
|
every { output.writeEntityData(data, data.size) } throws IOException()
|
||||||
|
@ -170,7 +207,14 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
getRecordsAndOutput()
|
getRecordsAndOutput()
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} returns versionHeader
|
||||||
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
||||||
every { output.writeEntityHeader(key, data.size) } returns 42
|
every { output.writeEntityHeader(key, data.size) } returns 42
|
||||||
every { output.writeEntityData(data, data.size) } returns data.size
|
every { output.writeEntityData(data, data.size) } returns data.size
|
||||||
|
@ -190,14 +234,28 @@ internal class KVRestoreTest : RestoreTest() {
|
||||||
// first key/value
|
// first key/value
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key64) } returns inputStream
|
||||||
every { headerReader.readVersion(inputStream) } returns VERSION
|
every { headerReader.readVersion(inputStream) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream, VERSION, packageInfo.packageName, key) } returns versionHeader
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
} returns versionHeader
|
||||||
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
every { crypto.decryptMultipleSegments(inputStream) } returns data
|
||||||
every { output.writeEntityHeader(key, data.size) } returns 42
|
every { output.writeEntityHeader(key, data.size) } returns 42
|
||||||
every { output.writeEntityData(data, data.size) } returns data.size
|
every { output.writeEntityData(data, data.size) } returns data.size
|
||||||
// second key/value
|
// second key/value
|
||||||
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key264) } returns inputStream2
|
coEvery { plugin.getInputStreamForRecord(token, packageInfo, key264) } returns inputStream2
|
||||||
every { headerReader.readVersion(inputStream2) } returns VERSION
|
every { headerReader.readVersion(inputStream2) } returns VERSION
|
||||||
every { crypto.decryptHeader(inputStream2, VERSION, packageInfo.packageName, key2) } returns versionHeader2
|
every {
|
||||||
|
crypto.decryptHeader(
|
||||||
|
inputStream2,
|
||||||
|
VERSION,
|
||||||
|
packageInfo.packageName,
|
||||||
|
key2
|
||||||
|
)
|
||||||
|
} returns versionHeader2
|
||||||
every { crypto.decryptMultipleSegments(inputStream2) } returns data2
|
every { crypto.decryptMultipleSegments(inputStream2) } returns data2
|
||||||
every { output.writeEntityHeader(key2, data2.size) } returns 42
|
every { output.writeEntityHeader(key2, data2.size) } returns 42
|
||||||
every { output.writeEntityData(data2, data2.size) } returns data2.size
|
every { output.writeEntityData(data2, data2.size) } returns data2.size
|
||||||
|
|
|
@ -63,8 +63,8 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
private val packageInfoArray = arrayOf(packageInfo)
|
private val packageInfoArray = arrayOf(packageInfo)
|
||||||
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
private val packageInfoArray2 = arrayOf(packageInfo, packageInfo2)
|
||||||
private val pmPackageInfoArray = arrayOf(
|
private val pmPackageInfoArray = arrayOf(
|
||||||
PackageInfo().apply { packageName = "@pm@" },
|
PackageInfo().apply { packageName = "@pm@" },
|
||||||
packageInfo
|
packageInfo
|
||||||
)
|
)
|
||||||
private val packageName = packageInfo.packageName
|
private val packageName = packageInfo.packageName
|
||||||
private val storageName = getRandomString()
|
private val storageName = getRandomString()
|
||||||
|
@ -73,12 +73,16 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
|
fun `getAvailableRestoreSets() builds set from plugin response`() = runBlocking {
|
||||||
val encryptedMetadata = EncryptedBackupMetadata(token, inputStream)
|
val encryptedMetadata = EncryptedBackupMetadata(token, inputStream)
|
||||||
val metadata = BackupMetadata(
|
val metadata = BackupMetadata(
|
||||||
token = token,
|
token = token,
|
||||||
androidVersion = Random.nextInt(),
|
androidVersion = Random.nextInt(),
|
||||||
androidIncremental = getRandomString(),
|
androidIncremental = getRandomString(),
|
||||||
deviceName = getRandomString())
|
deviceName = getRandomString()
|
||||||
|
)
|
||||||
|
|
||||||
coEvery { plugin.getAvailableBackups() } returns sequenceOf(encryptedMetadata, encryptedMetadata)
|
coEvery { plugin.getAvailableBackups() } returns sequenceOf(
|
||||||
|
encryptedMetadata,
|
||||||
|
encryptedMetadata
|
||||||
|
)
|
||||||
every { metadataReader.readMetadata(inputStream, token) } returns metadata
|
every { metadataReader.readMetadata(inputStream, token) } returns metadata
|
||||||
every { inputStream.close() } just Runs
|
every { inputStream.close() } just Runs
|
||||||
|
|
||||||
|
@ -126,11 +130,21 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
every { documentFile.isDirectory } returns false
|
every { documentFile.isDirectory } returns false
|
||||||
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
|
every { metadataManager.getPackageMetadata(packageName) } returns PackageMetadata(42L)
|
||||||
every { storage.name } returns storageName
|
every { storage.name } returns storageName
|
||||||
every { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) } just Runs
|
every {
|
||||||
|
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||||
|
packageName,
|
||||||
|
storageName
|
||||||
|
)
|
||||||
|
} just Runs
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
|
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
|
||||||
|
|
||||||
verify(exactly = 1) { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) }
|
verify(exactly = 1) {
|
||||||
|
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||||
|
packageName,
|
||||||
|
storageName
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -142,7 +156,12 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
assertEquals(TRANSPORT_OK, restore.startRestore(token, pmPackageInfoArray))
|
assertEquals(TRANSPORT_OK, restore.startRestore(token, pmPackageInfoArray))
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) }
|
verify(exactly = 0) {
|
||||||
|
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||||
|
packageName,
|
||||||
|
storageName
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -155,7 +174,12 @@ internal class RestoreCoordinatorTest : TransportTest() {
|
||||||
|
|
||||||
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
|
assertEquals(TRANSPORT_ERROR, restore.startRestore(token, pmPackageInfoArray))
|
||||||
|
|
||||||
verify(exactly = 0) { notificationManager.onRemovableStorageNotAvailableForRestore(packageName, storageName) }
|
verify(exactly = 0) {
|
||||||
|
notificationManager.onRemovableStorageNotAvailableForRestore(
|
||||||
|
packageName,
|
||||||
|
storageName
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue