Refactor code related to APK installs as preparation for upcoming changes

This commit is contained in:
Torsten Grote 2020-10-07 15:52:39 -03:00 committed by Chirayu Desai
parent 9830d2db95
commit f45411d81b
16 changed files with 117 additions and 105 deletions

View file

@ -13,6 +13,7 @@ import com.stevesoltys.seedvault.metadata.MetadataManager
import com.stevesoltys.seedvault.metadata.metadataModule
import com.stevesoltys.seedvault.plugins.saf.documentsProviderModule
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.restore.install.installModule
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.settings.SettingsViewModel
import com.stevesoltys.seedvault.transport.backup.backupModule
@ -60,6 +61,7 @@ class App : Application() {
documentsProviderModule, // storage plugin
backupModule,
restoreModule,
installModule,
appModule
)
)

View file

@ -5,6 +5,7 @@ import androidx.annotation.CallSuper
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.install.InstallProgressFragment
import com.stevesoltys.seedvault.ui.LiveEventHandler
import com.stevesoltys.seedvault.ui.RequireProvisioningActivity
import com.stevesoltys.seedvault.ui.RequireProvisioningViewModel

View file

@ -37,10 +37,10 @@ import com.stevesoltys.seedvault.ui.AppBackupState.NOT_YET_BACKED_UP
import com.stevesoltys.seedvault.ui.AppBackupState.SUCCEEDED
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_APPS
import com.stevesoltys.seedvault.restore.DisplayFragment.RESTORE_BACKUP
import com.stevesoltys.seedvault.restore.install.InstallResult
import com.stevesoltys.seedvault.settings.SettingsManager
import com.stevesoltys.seedvault.transport.TRANSPORT_ID
import com.stevesoltys.seedvault.transport.restore.ApkRestore
import com.stevesoltys.seedvault.transport.restore.InstallResult
import com.stevesoltys.seedvault.restore.install.ApkRestore
import com.stevesoltys.seedvault.transport.restore.RestoreCoordinator
import com.stevesoltys.seedvault.ui.LiveEvent
import com.stevesoltys.seedvault.ui.MutableLiveEvent

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.transport.restore
package com.stevesoltys.seedvault.restore.install
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
@ -17,8 +17,8 @@ import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL
import android.content.pm.PackageManager
import android.util.Log
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
@ -111,7 +111,7 @@ internal class ApkInstaller(private val context: Context) {
// update status and offer result
val status = if (success) SUCCEEDED else FAILED
return installResult.update(packageName) { it.copy(status = status) }
return installResult.update(packageName) { it.copy(state = status) }
}
}

View file

@ -1,33 +1,32 @@
package com.stevesoltys.seedvault.transport.restore
package com.stevesoltys.seedvault.restore.install
import android.content.Context
import android.content.pm.PackageManager.GET_SIGNATURES
import android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES
import android.content.pm.PackageManager.NameNotFoundException
import android.graphics.drawable.Drawable
import android.util.Log
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.transport.backup.copyStreamsAndGetHash
import com.stevesoltys.seedvault.transport.backup.getSignatures
import com.stevesoltys.seedvault.transport.backup.isSystemApp
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import java.io.File
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
private val TAG = ApkRestore::class.java.simpleName
internal class ApkRestore(
private val context: Context,
private val restorePlugin: RestorePlugin,
private val apkInstaller: ApkInstaller = ApkInstaller(context)
private val apkInstaller: ApkInstaller
) {
private val pm = context.packageManager
@ -43,7 +42,7 @@ internal class ApkRestore(
val installResult = MutableInstallResult(total)
packages.forEach { (packageName, _) ->
progress++
installResult[packageName] = ApkRestoreResult(packageName, progress, total, QUEUED)
installResult[packageName] = ApkInstallResult(packageName, progress, total, QUEUED)
}
emit(installResult)
@ -123,7 +122,7 @@ internal class ApkRestore(
installResult.update(packageName) { result ->
result.copy(
status = IN_PROGRESS,
state = IN_PROGRESS,
name = name,
icon = icon
)
@ -152,46 +151,7 @@ internal class ApkRestore(
}
private fun fail(installResult: MutableInstallResult, packageName: String): InstallResult {
return installResult.update(packageName) { it.copy(status = FAILED) }
return installResult.update(packageName) { it.copy(state = FAILED) }
}
}
internal typealias InstallResult = Map<String, ApkRestoreResult>
internal fun InstallResult.getInProgress(): ApkRestoreResult? {
val filtered = filterValues { result -> result.status == IN_PROGRESS }
if (filtered.isEmpty()) return null
check(filtered.size == 1) { "More than one package in progress: ${filtered.keys}" }
return filtered.values.first()
}
internal class MutableInstallResult(initialCapacity: Int) :
ConcurrentHashMap<String, ApkRestoreResult>(initialCapacity) {
fun update(
packageName: String,
updateFun: (ApkRestoreResult) -> ApkRestoreResult
): MutableInstallResult {
val result = get(packageName)
check(result != null) { "ApkRestoreResult for $packageName does not exist." }
set(packageName, updateFun(result))
return this
}
}
internal data class ApkRestoreResult(
val packageName: CharSequence,
val progress: Int,
val total: Int,
val status: ApkRestoreStatus,
val name: CharSequence? = null,
val icon: Drawable? = null
) : Comparable<ApkRestoreResult> {
override fun compareTo(other: ApkRestoreResult): Int {
return other.progress.compareTo(progress)
}
}
internal enum class ApkRestoreStatus {
QUEUED, IN_PROGRESS, SUCCEEDED, FAILED
}

View file

@ -0,0 +1,9 @@
package com.stevesoltys.seedvault.restore.install
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val installModule = module {
factory { ApkInstaller(androidContext()) }
factory { ApkRestore(androidContext(), get(), get()) }
}

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore
package com.stevesoltys.seedvault.restore.install
import android.view.LayoutInflater
import android.view.View
@ -9,25 +9,24 @@ import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.SortedList
import androidx.recyclerview.widget.SortedListAdapterCallback
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.restore.ApkRestoreResult
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.ui.AppViewHolder
internal class InstallProgressAdapter : Adapter<AppInstallViewHolder>() {
private val items = SortedList<ApkRestoreResult>(
ApkRestoreResult::class.java,
object : SortedListAdapterCallback<ApkRestoreResult>(this) {
override fun areItemsTheSame(item1: ApkRestoreResult, item2: ApkRestoreResult) =
private val items = SortedList<ApkInstallResult>(
ApkInstallResult::class.java,
object : SortedListAdapterCallback<ApkInstallResult>(this) {
override fun areItemsTheSame(item1: ApkInstallResult, item2: ApkInstallResult) =
item1.packageName == item2.packageName
override fun areContentsTheSame(oldItem: ApkRestoreResult, newItem: ApkRestoreResult) =
override fun areContentsTheSame(oldItem: ApkInstallResult, newItem: ApkInstallResult) =
oldItem == newItem
override fun compare(item1: ApkRestoreResult, item2: ApkRestoreResult) =
override fun compare(item1: ApkInstallResult, item2: ApkInstallResult) =
item1.compareTo(item2)
})
@ -43,17 +42,17 @@ internal class InstallProgressAdapter : Adapter<AppInstallViewHolder>() {
holder.bind(items[position])
}
fun update(items: Collection<ApkRestoreResult>) {
fun update(items: Collection<ApkInstallResult>) {
this.items.replaceAll(items)
}
}
internal class AppInstallViewHolder(v: View) : AppViewHolder(v) {
fun bind(item: ApkRestoreResult) {
fun bind(item: ApkInstallResult) {
appIcon.setImageDrawable(item.icon)
appName.text = item.name
when (item.status) {
when (item.state) {
IN_PROGRESS -> {
appStatus.visibility = INVISIBLE
progressBar.visibility = VISIBLE

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.restore
package com.stevesoltys.seedvault.restore.install
import android.os.Bundle
import android.view.LayoutInflater
@ -14,9 +14,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager.VERTICAL
import androidx.recyclerview.widget.RecyclerView
import com.stevesoltys.seedvault.R
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
import com.stevesoltys.seedvault.transport.restore.InstallResult
import com.stevesoltys.seedvault.transport.restore.getInProgress
import com.stevesoltys.seedvault.restore.RestoreViewModel
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
class InstallProgressFragment : Fragment() {
@ -76,7 +75,7 @@ class InstallProgressFragment : Fragment() {
// skip this screen, if there are no apps to install
if (installResult.isEmpty()) viewModel.onNextClicked()
val result = installResult.filterValues { it.status != QUEUED }
val result = installResult.filterValues { it.state != QUEUED }
val position = layoutManager.findFirstVisibleItemPosition()
adapter.update(result.values)
if (position == 0) layoutManager.scrollToPosition(0)

View file

@ -0,0 +1,46 @@
package com.stevesoltys.seedvault.restore.install
import android.graphics.drawable.Drawable
import java.util.concurrent.ConcurrentHashMap
internal typealias InstallResult = Map<String, ApkInstallResult>
internal fun InstallResult.getInProgress(): ApkInstallResult? {
val filtered = filterValues { result -> result.state == ApkInstallState.IN_PROGRESS }
if (filtered.isEmpty()) return null
check(filtered.size == 1) { "More than one package in progress: ${filtered.keys}" }
return filtered.values.first()
}
internal class MutableInstallResult(initialCapacity: Int) :
ConcurrentHashMap<String, ApkInstallResult>(initialCapacity) {
fun update(
packageName: String,
updateFun: (ApkInstallResult) -> ApkInstallResult
): MutableInstallResult {
val result = get(packageName)
check(result != null) { "ApkRestoreResult for $packageName does not exist." }
set(packageName, updateFun(result))
return this
}
}
internal data class ApkInstallResult(
val packageName: CharSequence,
val progress: Int,
val total: Int,
val state: ApkInstallState,
val name: CharSequence? = null,
val icon: Drawable? = null
) : Comparable<ApkInstallResult> {
override fun compareTo(other: ApkInstallResult): Int {
return other.progress.compareTo(progress)
}
}
internal enum class ApkInstallState {
QUEUED,
IN_PROGRESS,
SUCCEEDED,
FAILED
}

View file

@ -5,7 +5,6 @@ import org.koin.dsl.module
val restoreModule = module {
single { OutputFactory() }
factory { ApkRestore(androidContext(), get()) }
single { KVRestore(get<RestorePlugin>().kvRestorePlugin, get(), get(), get()) }
single { FullRestore(get<RestorePlugin>().fullRestorePlugin, get(), get(), get()) }
single { RestoreCoordinator(androidContext(), get(), get(), get(), get(), get(), get(), get()) }

View file

@ -1,4 +1,4 @@
package com.stevesoltys.seedvault.transport.restore
package com.stevesoltys.seedvault.restore.install
import android.content.Context
import android.content.pm.ApplicationInfo
@ -11,10 +11,12 @@ import android.graphics.drawable.Drawable
import com.stevesoltys.seedvault.getRandomString
import com.stevesoltys.seedvault.metadata.PackageMetadata
import com.stevesoltys.seedvault.metadata.PackageMetadataMap
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.FAILED
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.IN_PROGRESS
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.QUEUED
import com.stevesoltys.seedvault.transport.restore.ApkRestoreStatus.SUCCEEDED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.FAILED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.IN_PROGRESS
import com.stevesoltys.seedvault.restore.install.ApkInstallState.QUEUED
import com.stevesoltys.seedvault.restore.install.ApkInstallState.SUCCEEDED
import com.stevesoltys.seedvault.transport.TransportTest
import com.stevesoltys.seedvault.transport.restore.RestorePlugin
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
@ -34,7 +36,7 @@ import kotlin.random.Random
@Suppress("BlockingMethodInNonBlockingContext")
@ExperimentalCoroutinesApi
internal class ApkRestoreTest : RestoreTest() {
internal class ApkRestoreTest : TransportTest() {
private val pm: PackageManager = mockk()
private val strictContext: Context = mockk<Context>().apply {
@ -79,13 +81,13 @@ internal class ApkRestoreTest : RestoreTest() {
when (index) {
0 -> {
val result = value[packageName] ?: fail()
assertEquals(QUEUED, result.status)
assertEquals(QUEUED, result.state)
assertEquals(1, result.progress)
assertEquals(1, result.total)
}
1 -> {
val result = value[packageName] ?: fail()
assertEquals(FAILED, result.status)
assertEquals(FAILED, result.state)
}
else -> fail()
}
@ -105,13 +107,13 @@ internal class ApkRestoreTest : RestoreTest() {
when (index) {
0 -> {
val result = value[packageName] ?: fail()
assertEquals(QUEUED, result.status)
assertEquals(QUEUED, result.state)
assertEquals(1, result.progress)
assertEquals(1, result.total)
}
1 -> {
val result = value[packageName] ?: fail()
assertEquals(FAILED, result.status)
assertEquals(FAILED, result.state)
}
else -> fail()
}
@ -143,19 +145,19 @@ internal class ApkRestoreTest : RestoreTest() {
when (index) {
0 -> {
val result = value[packageName] ?: fail()
assertEquals(QUEUED, result.status)
assertEquals(QUEUED, result.state)
assertEquals(1, result.progress)
assertEquals(1, result.total)
}
1 -> {
val result = value[packageName] ?: fail()
assertEquals(IN_PROGRESS, result.status)
assertEquals(IN_PROGRESS, result.state)
assertEquals(appName, result.name)
assertEquals(icon, result.icon)
}
2 -> {
val result = value[packageName] ?: fail()
assertEquals(FAILED, result.status)
assertEquals(FAILED, result.state)
}
else -> fail()
}
@ -166,11 +168,11 @@ internal class ApkRestoreTest : RestoreTest() {
fun `test successful run`(@TempDir tmpDir: Path) = runBlocking {
val installResult = MutableInstallResult(1).apply {
put(
packageName, ApkRestoreResult(
packageName, ApkInstallResult(
packageName,
progress = 1,
total = 1,
status = SUCCEEDED
state = SUCCEEDED
)
)
}
@ -194,19 +196,19 @@ internal class ApkRestoreTest : RestoreTest() {
when (i) {
0 -> {
val result = value[packageName] ?: fail()
assertEquals(QUEUED, result.status)
assertEquals(QUEUED, result.state)
assertEquals(1, result.progress)
assertEquals(1, result.total)
}
1 -> {
val result = value[packageName] ?: fail()
assertEquals(IN_PROGRESS, result.status)
assertEquals(IN_PROGRESS, result.state)
assertEquals(appName, result.name)
assertEquals(icon, result.icon)
}
2 -> {
val result = value[packageName] ?: fail()
assertEquals(SUCCEEDED, result.status)
assertEquals(SUCCEEDED, result.state)
}
else -> fail()
}
@ -244,7 +246,7 @@ internal class ApkRestoreTest : RestoreTest() {
val installResult = MutableInstallResult(1).apply {
put(
packageName,
ApkRestoreResult(packageName, progress = 1, total = 1, status = SUCCEEDED)
ApkInstallResult(packageName, progress = 1, total = 1, state = SUCCEEDED)
)
}
every {
@ -262,22 +264,22 @@ internal class ApkRestoreTest : RestoreTest() {
when (i) {
0 -> {
val result = value[packageName] ?: fail()
assertEquals(QUEUED, result.status)
assertEquals(QUEUED, result.state)
assertEquals(1, result.progress)
assertEquals(1, result.total)
}
1 -> {
val result = value[packageName] ?: fail()
assertEquals(IN_PROGRESS, result.status)
assertEquals(IN_PROGRESS, result.state)
assertEquals(appName, result.name)
assertEquals(icon, result.icon)
}
2 -> {
val result = value[packageName] ?: fail()
if (willFail) {
assertEquals(FAILED, result.status)
assertEquals(FAILED, result.state)
} else {
assertEquals(SUCCEEDED, result.status)
assertEquals(SUCCEEDED, result.state)
}
}
else -> fail()

View file

@ -103,7 +103,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
private val backupDataInput = mockk<BackupDataInput>()
private val fileDescriptor = mockk<ParcelFileDescriptor>(relaxed = true)
private val token = Random.nextLong()
private val appData = ByteArray(42).apply { Random.nextBytes(this) }
private val appData2 = ByteArray(1337).apply { Random.nextBytes(this) }
private val metadataOutputStream = ByteArrayOutputStream()

View file

@ -29,6 +29,7 @@ abstract class TransportTest {
protected val context = mockk<Context>(relaxed = true)
protected val sigInfo: SigningInfo = mockk()
protected val token = Random.nextLong()
protected val packageInfo = PackageInfo().apply {
packageName = "org.example"
longVersionCode = Random.nextLong()

View file

@ -6,7 +6,6 @@ import com.stevesoltys.seedvault.header.VersionHeader
import com.stevesoltys.seedvault.transport.TransportTest
import io.mockk.mockk
import java.io.OutputStream
import kotlin.random.Random
internal abstract class BackupTest : TransportTest() {
@ -15,7 +14,6 @@ internal abstract class BackupTest : TransportTest() {
protected val data = mockk<ParcelFileDescriptor>()
protected val outputStream = mockk<OutputStream>()
protected val token = Random.nextLong()
protected val header = VersionHeader(packageName = packageInfo.packageName)
protected val quota = 42L

View file

@ -55,7 +55,6 @@ internal class RestoreCoordinatorTest : TransportTest() {
metadataReader
)
private val token = Random.nextLong()
private val inputStream = mockk<InputStream>()
private val storage: Storage = mockk()
private val documentFile: DocumentFile = mockk()

View file

@ -7,7 +7,6 @@ import com.stevesoltys.seedvault.header.VERSION
import com.stevesoltys.seedvault.transport.TransportTest
import io.mockk.mockk
import java.io.InputStream
import kotlin.random.Random
internal abstract class RestoreTest : TransportTest() {
@ -15,7 +14,6 @@ internal abstract class RestoreTest : TransportTest() {
protected val headerReader = mockk<HeaderReader>()
protected val fileDescriptor = mockk<ParcelFileDescriptor>()
protected val token = Random.nextLong()
protected val data = getRandomByteArray()
protected val inputStream = mockk<InputStream>()