Skip to content

Commit

Permalink
Work vending: Show installation success without reload
Browse files Browse the repository at this point in the history
  • Loading branch information
fynngodau committed Sep 22, 2024
1 parent 73f1a3a commit 3b8509d
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import java.io.File

private const val TAG = "GmsVendingComponentDl"

@RequiresApi(Build.VERSION_CODES.M)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
suspend fun HttpClient.downloadPackageComponents(
context: Context,
downloadList: List<PackageComponent>,
Expand Down
23 changes: 1 addition & 22 deletions vending-app/src/main/java/org/microg/vending/enterprise/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,6 @@ open class App(
val packageName: String,
val versionCode: Int?,
val displayName: String,
val state: State,
val iconUrl: String?,
val deliveryToken: String?
) {
enum class State {
/**
* App cannot be installed on this user's device
*/
NOT_COMPATIBLE,
/**
* App is available, but not installed on the user's device.
*/
NOT_INSTALLED,
/**
* App is already installed on the device, but an update is available.
*/
UPDATE_AVAILABLE,

/**
* App is installed on device and up to date.
*/
INSTALLED
}
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.microg.vending.enterprise

enum class AppState {
/**
* App cannot be installed on this user's device
*/
NOT_COMPATIBLE,
/**
* App is available, but not installed on the user's device.
*/
NOT_INSTALLED,
/**
* App is already installed on the device, but an update is available.
*/
UPDATE_AVAILABLE,
/**
* An app operation is currently outstanding
*/
PENDING,
/**
* App is installed on device and up to date.
*/
INSTALLED
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ class EnterpriseApp(
packageName: String,
versionCode: Int?,
displayName: String,
state: State,
iconUrl: String?,
deliveryToken: String?,
val policy: AppInstallPolicy
) : App(packageName, versionCode, displayName, state, iconUrl, deliveryToken)
) : App(packageName, versionCode, displayName, iconUrl, deliveryToken)
67 changes: 39 additions & 28 deletions vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
Expand All @@ -17,6 +16,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -48,14 +48,15 @@ import org.microg.vending.delivery.downloadPackageComponents
import org.microg.vending.enterprise.App
import org.microg.vending.enterprise.EnterpriseApp
import org.microg.vending.delivery.requestDownloadUrls
import org.microg.vending.enterprise.AppState
import org.microg.vending.ui.components.EnterpriseList
import org.microg.vending.ui.components.NetworkState
import java.io.IOException

@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
class VendingActivity : ComponentActivity() {

var apps: MutableList<EnterpriseApp> = mutableStateListOf()
var apps: MutableMap<EnterpriseApp, AppState> = mutableStateMapOf()
var networkState by mutableStateOf(NetworkState.ACTIVE)

var auth: AuthData? = null
Expand All @@ -79,38 +80,45 @@ class VendingActivity : ComponentActivity() {
load(account)

val install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit = { app, isUpdate ->
Toast.makeText(this, "installing ${app.displayName} / ${app.packageName}", Toast.LENGTH_SHORT).show()
Thread {
runBlocking {

val previousState = apps[app]!!
apps[app] = AppState.PENDING

val client = HttpClient(this@VendingActivity)

// Get download links for requested package
val downloadUrls = client.requestDownloadUrls(
val downloadUrls = runCatching { client.requestDownloadUrls(
app.packageName,
app.versionCode!!.toLong(),
auth!!,
deliveryToken = app.deliveryToken
)
) }

if (downloadUrls.isFailure) {
Log.w(TAG, "Failed to request download URLs: ${downloadUrls.exceptionOrNull()!!.message}")
apps[app] = previousState
return@runBlocking
}

val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls, Unit)
val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls.getOrThrow(), Unit)
if (packageFiles.values.any { it == null }) {
Log.w(TAG, "Cannot proceed to installation as not all files were downloaded")
apps[app] = previousState
return@runBlocking
}

val successfullyInstalled = runCatching {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
installPackages(
app.packageName,
packageFiles.values.filterNotNull(),
isUpdate
)
} else {
TODO("implement installation on Lollipop installation")
}
runCatching {
installPackages(
app.packageName,
packageFiles.values.filterNotNull(),
isUpdate
)
}.onSuccess {
load(account)
apps[app] = AppState.INSTALLED
}.onFailure {
apps[app] = previousState
}
}
}.start()
Expand All @@ -120,8 +128,12 @@ class VendingActivity : ComponentActivity() {
Thread {
runBlocking {

val previousState = apps[app]!!
apps[app] = AppState.PENDING
runCatching { uninstallPackage(app.packageName) }.onSuccess {
load(account)
apps[app] = AppState.NOT_INSTALLED
}.onFailure {
apps[app] = previousState
}

}
Expand Down Expand Up @@ -190,7 +202,7 @@ class VendingActivity : ComponentActivity() {
RequestItem(RequestApp(AppMeta(it.packageName)))
}
)
).items.map { it.response }.filterNotNull().map { item ->
).items.map { it.response }.filterNotNull().associate { item ->
val packageName = item.meta!!.packageName!!
val installedDetails = this@VendingActivity.packageManager.getInstalledPackages(0).find {
it.applicationInfo.packageName == packageName
Expand All @@ -202,28 +214,27 @@ class VendingActivity : ComponentActivity() {
item.offer!!.version!!.versionCode!!
} else null

val state = if (!available && installedDetails == null) App.State.NOT_COMPATIBLE
else if (!available && installedDetails != null) App.State.INSTALLED
else if (available && installedDetails == null) App.State.NOT_INSTALLED
else if (available && installedDetails != null && installedDetails.versionCode > versionCode!!) App.State.UPDATE_AVAILABLE
else /* if (available && installedDetails != null) */ App.State.INSTALLED
val state = if (!available && installedDetails == null) AppState.NOT_COMPATIBLE
else if (!available && installedDetails != null) AppState.INSTALLED
else if (available && installedDetails == null) AppState.NOT_INSTALLED
else if (available && installedDetails != null && installedDetails.versionCode > versionCode!!) AppState.UPDATE_AVAILABLE
else /* if (available && installedDetails != null) */ AppState.INSTALLED

EnterpriseApp(
packageName,
versionCode,
item.detail!!.name!!.displayName!!,
state,
item.detail.icon?.icon?.paint?.url,
item.offer?.delivery?.key,
apps.find { it.packageName!! == item.meta.packageName }!!.policy!!,
)
) to state
}.onEach {
Log.v(TAG, "${it.packageName} delivery token: ${it.deliveryToken ?: "none acquired"}")
Log.v(TAG, "${it.key.packageName} (state: ${it.value}) delivery token: ${it.key.deliveryToken ?: "none acquired"}")
}

this@VendingActivity.apps.apply {
clear()
addAll(details)
putAll(details)
}
networkState = NetworkState.PASSIVE
} catch (e: IOException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -24,9 +25,10 @@ import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.android.vending.R
import org.microg.vending.enterprise.App
import org.microg.vending.enterprise.AppState

@Composable
fun AppRow(app: App, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) {
fun AppRow(app: App, state: AppState, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) {
Row(
Modifier.padding(top = 8.dp, bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start),
Expand All @@ -45,48 +47,59 @@ fun AppRow(app: App, install: () -> Unit, update: () -> Unit, uninstall: () -> U
Text(app.displayName)

Spacer(Modifier.weight(1f))
if (app.state == App.State.NOT_COMPATIBLE) {
if (state == AppState.NOT_COMPATIBLE) {
Icon(Icons.Default.Warning, null, Modifier.padding(end=8.dp), tint = MaterialTheme.colorScheme.secondary)
// TODO better UI
}
if (app.state == App.State.UPDATE_AVAILABLE || app.state == App.State.INSTALLED) {
if (state == AppState.UPDATE_AVAILABLE || state == AppState.INSTALLED) {
IconButton(uninstall) {
Icon(Icons.Default.Delete, stringResource(R.string.vending_overview_row_action_uninstall), tint = MaterialTheme.colorScheme.secondary)
}
}
if (app.state == App.State.UPDATE_AVAILABLE) {
if (state == AppState.UPDATE_AVAILABLE) {
FilledIconButton(update, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) {
Icon(painterResource(R.drawable.ic_update), stringResource(R.string.vending_overview_row_action_update), tint = MaterialTheme.colorScheme.secondary)
}
}
if (app.state == App.State.NOT_INSTALLED)
FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) {
Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary)
if (state == AppState.NOT_INSTALLED) {
FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) {
Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary)
}
}
if (state == AppState.PENDING) {
CircularProgressIndicator(Modifier.padding(4.dp))
}
}

}

private val previewApp = App("org.mozilla.firefox", 0, "Firefox", null, null)
@Preview
@Composable
fun AppRowNotCompatiblePreview() {
AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_COMPATIBLE, null, null), {}, {}, {})
AppRow(previewApp, AppState.NOT_COMPATIBLE, {}, {}, {})
}

@Preview
@Composable
fun AppRowNotInstalledPreview() {
AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, ""), {}, {}, {})
AppRow(previewApp, AppState.NOT_INSTALLED, {}, {}, {})
}

@Preview
@Composable
fun AppRowUpdateablePreview() {
AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.UPDATE_AVAILABLE, null, ""), {}, {}, {})
AppRow(previewApp, AppState.UPDATE_AVAILABLE, {}, {}, {})
}

@Preview
@Composable
fun AppRowInstalledPreview() {
AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.INSTALLED, null, ""), {}, {}, {})
AppRow(previewApp, AppState.INSTALLED, {}, {}, {})
}

@Preview
@Composable
fun AppRowPendingPreview() {
AppRow(previewApp, AppState.PENDING, {}, {}, {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,24 +26,30 @@ import androidx.compose.ui.unit.dp
import com.android.vending.R
import com.google.android.finsky.AppInstallPolicy
import org.microg.vending.enterprise.App
import org.microg.vending.enterprise.AppState
import org.microg.vending.enterprise.EnterpriseApp


@Composable
fun EnterpriseList(apps: List<EnterpriseApp>, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) {
if (apps.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) {
fun EnterpriseList(appStates: Map<EnterpriseApp, AppState>, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) {
if (appStates.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) {

val apps = appStates.keys
val requiredApps = apps.filter { it.policy == AppInstallPolicy.MANDATORY }
if (requiredApps.isNotEmpty()) {
item { InListHeading(R.string.vending_overview_enterprise_row_mandatory) }
item { InListWarning(R.string.vending_overview_enterprise_row_mandatory_hint) }
items(requiredApps) { AppRow(it, { install(it, false) }, { install(it, true) }, { uninstall(it) }) }
items(requiredApps.sortedBy { it.packageName }) {
AppRow(it, appStates[it]!!, { install(it, false) }, { install(it, true) }, { uninstall(it) })
}
}

val optionalApps = apps.filter { it.policy == AppInstallPolicy.OPTIONAL }
if (optionalApps.isNotEmpty()) {
item { InListHeading(R.string.vending_overview_enterprise_row_offered) }
items(optionalApps) { AppRow(it, { install(it, false) }, { install(it, true) }, { uninstall(it) }) }
items(optionalApps.sortedBy { it.packageName }) {
AppRow(it, appStates[it]!!, { install(it, false) }, { install(it, true) }, { uninstall(it) })
}
}

} else Box(
Expand Down Expand Up @@ -99,16 +105,16 @@ fun InListWarning(@StringRes text: Int) {
@Composable
fun EnterpriseListPreview() {
EnterpriseList(
listOf(
EnterpriseApp("com.android.vending", 0, "Market", App.State.INSTALLED, null, "", AppInstallPolicy.MANDATORY),
EnterpriseApp("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, "", AppInstallPolicy.OPTIONAL),
EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", App.State.NOT_COMPATIBLE, null, "", AppInstallPolicy.OPTIONAL)
mapOf(
EnterpriseApp("com.android.vending", 0, "Market", null, "", AppInstallPolicy.MANDATORY) to AppState.INSTALLED,
EnterpriseApp("org.mozilla.firefox", 0, "Firefox", null, "", AppInstallPolicy.OPTIONAL) to AppState.NOT_INSTALLED,
EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", null, "", AppInstallPolicy.OPTIONAL) to AppState.NOT_COMPATIBLE
), { _, _ -> }, {}
)
}

@Preview
@Composable
fun EnterpriseListEmptyPreview() {
EnterpriseList(emptyList(), { _, _ -> }, {})
EnterpriseList(emptyMap(), { _, _ -> }, {})
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import java.io.File
import java.io.FileInputStream
import java.io.IOException

@RequiresApi(Build.VERSION_CODES.M)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
internal suspend fun Context.installPackages(
callingPackage: String,
componentFiles: List<File>,
Expand Down

0 comments on commit 3b8509d

Please sign in to comment.