diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml
index d9de88954..7b82f61d3 100644
--- a/vending-app/src/main/AndroidManifest.xml
+++ b/vending-app/src/main/AndroidManifest.xml
@@ -187,8 +187,8 @@
+ android:name=".installer.SessionResultReceiver"
+ android:exported="false"/>
+ suspend fun download(url: String, downloadFile: File, tag: Any): String = suspendCoroutine { continuation ->
val uriBuilder = Uri.parse(url).buildUpon()
requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) {
override fun parseNetworkResponse(response: NetworkResponse): Response {
diff --git a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt
new file mode 100644
index 000000000..85aef50ee
--- /dev/null
+++ b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt
@@ -0,0 +1,40 @@
+package org.microg.vending.delivery
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import com.android.vending.installer.packageDownloadLocation
+import com.google.android.finsky.splitinstallservice.PackageComponent
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import org.microg.vending.billing.core.HttpClient
+import java.io.File
+
+private const val TAG = "GmsVendingComponentDl"
+
+@RequiresApi(Build.VERSION_CODES.M)
+suspend fun HttpClient.downloadPackageComponents(
+ context: Context,
+ downloadList: List,
+ tag: Any
+): Map = coroutineScope {
+ downloadList.map { info ->
+ Log.d(TAG, "downloadSplitPackage: $info")
+ async {
+ info to runCatching {
+ val file = File(context.packageDownloadLocation().toString(), info.componentName)
+ download(
+ url = info.url,
+ downloadFile = file,
+ tag = tag
+ )
+ file
+ }.onFailure {
+ Log.w(TAG, "package component failed to downlaod from url ${info.url}, " +
+ "to be saved as `${info.componentName}`", it)
+ }.getOrNull()
+ }
+ }.awaitAll().associate { it }
+}
diff --git a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt
new file mode 100644
index 000000000..bcb774edf
--- /dev/null
+++ b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt
@@ -0,0 +1,79 @@
+package org.microg.vending.delivery
+
+import android.util.Log
+import com.android.vending.buildRequestHeaders
+import com.google.android.finsky.GoogleApiResponse
+import com.google.android.finsky.splitinstallservice.PackageComponent
+import org.microg.vending.billing.core.AuthData
+import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY
+import org.microg.vending.billing.core.HttpClient
+import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG
+
+private const val TAG = "GmsVendingDelivery"
+
+/**
+ * Call the FDFE delivery endpoint to retrieve download URLs for the
+ * desired components. If specific split install packages are requested,
+ * only those will be contained in the result.
+ */
+suspend fun HttpClient.requestDownloadUrls(
+ packageName: String,
+ versionCode: Long,
+ auth: AuthData,
+ requestSplitPackages: List? = null,
+ deliveryToken: String? = null,
+): List {
+
+ val requestUrl = StringBuilder("$URL_DELIVERY?doc=$packageName&ot=1&vc=$versionCode")
+
+ requestSplitPackages?.apply {
+ requestUrl.append(
+ "&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch="
+ )
+ forEach { requestUrl.append("&mn=").append(it) }
+ }
+
+ deliveryToken?.let {
+ requestUrl.append("&dtok=$it")
+ }
+
+ Log.v(TAG, "requestDownloadUrls start")
+ val languages = requestSplitPackages?.filter { it.startsWith(SPLIT_LANGUAGE_TAG) }?.map {
+ it.replace(SPLIT_LANGUAGE_TAG, "")
+ }
+ Log.d(TAG, "requestDownloadUrls languages: $languages")
+
+ val response = get(
+ url = requestUrl.toString(),
+ headers = buildRequestHeaders(auth.authToken, auth.gsfId.toLong(16), languages),
+ adapter = GoogleApiResponse.ADAPTER
+ )
+ Log.d(TAG, "requestDownloadUrls end response -> $response")
+
+ val basePackage = response.response!!.splitReqResult!!.pkgList?.let {
+ if (it.baseUrl != null && it.baseBytes != null) {
+ PackageComponent(packageName, "base", it.baseUrl, it.baseBytes)
+ } else null
+ }
+ val splitComponents = response.response.splitReqResult!!.pkgList!!.pkgDownLoadInfo.filter {
+ !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty()
+ }.map {
+ if (requestSplitPackages != null) {
+ // Only download requested, if specific components were requested
+ requestSplitPackages.firstOrNull { requestComponent ->
+ requestComponent.contains(it.splitPkgName!!)
+ }?.let { requestComponent ->
+ PackageComponent(packageName, requestComponent, it.downloadUrl!!, it.size!!)
+ }
+ } else {
+ // Download all offered components (server chooses)
+ PackageComponent(packageName, it.splitPkgName!!, it.downloadUrl!!, it.size!!)
+ }
+ }
+
+ val components = (listOf(basePackage) + splitComponents).filterNotNull()
+
+ Log.d(TAG, "requestDownloadUrls end -> $components")
+
+ return components
+}
\ No newline at end of file
diff --git a/vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt b/vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt
new file mode 100644
index 000000000..d4ab71843
--- /dev/null
+++ b/vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt
@@ -0,0 +1,3 @@
+package org.microg.vending.splitinstall
+
+const val SPLIT_LANGUAGE_TAG = "config."
diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt
index 6dd0647e8..bd21b802a 100644
--- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt
+++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt
@@ -26,11 +26,9 @@ import com.android.vending.GetItemsResponse
import com.android.vending.RequestApp
import com.android.vending.RequestItem
import com.android.vending.buildRequestHeaders
+import com.android.vending.installer.installPackages
import com.android.volley.VolleyError
import com.google.android.finsky.GoogleApiResponse
-import com.google.android.finsky.splitinstallservice.DownloadStatus
-import com.google.android.finsky.splitinstallservice.PackageComponent
-import com.google.android.finsky.splitinstallservice.SplitInstallManager
import com.android.vending.installer.uninstallPackage
import kotlinx.coroutines.runBlocking
import org.microg.gms.common.DeviceConfiguration
@@ -41,14 +39,15 @@ import org.microg.gms.ui.TAG
import org.microg.vending.UploadDeviceConfigRequest
import org.microg.vending.billing.AuthManager
import org.microg.vending.billing.core.AuthData
-import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY
import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY
import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_FDFE
import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS
import org.microg.vending.billing.core.HttpClient
import org.microg.vending.billing.createDeviceEnvInfo
+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.ui.components.EnterpriseList
import org.microg.vending.ui.components.NetworkState
import java.io.IOException
@@ -83,42 +82,50 @@ class VendingActivity : ComponentActivity() {
Toast.makeText(this, "installing ${app.displayName} / ${app.packageName}", Toast.LENGTH_SHORT).show()
Thread {
runBlocking {
+
+ val client = HttpClient(this@VendingActivity)
+
// Get download links for requested package
- val res = HttpClient(this@VendingActivity).get(
- url = URL_DELIVERY,
- headers = buildRequestHeaders(auth!!.authToken, auth!!.gsfId.toLong(16)),
- params = mapOf(
- "ot" to "1",
- "doc" to app.packageName,
- "vc" to app.versionCode!!.toString()
- ).plus(app.deliveryToken?.let { listOf("dtok" to it) } ?: emptyList()),
- adapter = GoogleApiResponse.ADAPTER
+ val downloadUrls = client.requestDownloadUrls(
+ app.packageName,
+ app.versionCode!!.toLong(),
+ auth!!,
+ deliveryToken = app.deliveryToken
)
- Log.d(TAG, res.toString())
- val components = listOf(
- PackageComponent(app.packageName, "base", res.response!!.splitReqResult!!.pkgList!!.baseUrl!!)
- ) + res.response.splitReqResult!!.pkgList!!.pkgDownLoadInfo.map {
- PackageComponent(app.packageName, it.splitPkgName!!, it.downloadUrl!!)
- }
+ val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls, Unit)
+ if (packageFiles.values.any { it == null }) {
+ Log.w(TAG, "Cannot proceed to installation as not all files were downloaded")
+ return@runBlocking
+ }
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
- SplitInstallManager(this@VendingActivity).apply {
- components.forEach {
- SplitInstallManager.splitInstallRecord[it] = DownloadStatus.PENDING
- }
- notify(app.packageName)
- downloadAndInstall(app.packageName, components, isUpdate)
+ 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")
}
- } else {
- TODO("implement installation on Lollipop devices")
+ }.onSuccess {
+ load(account)
}
}
}.start()
}
- val uninstall: (app: EnterpriseApp) -> Unit = {
- uninstallPackage(it.packageName)
+ val uninstall: (app: EnterpriseApp) -> Unit = { app ->
+ Thread {
+ runBlocking {
+
+ runCatching { uninstallPackage(app.packageName) }.onSuccess {
+ load(account)
+ }
+
+ }
+ }.start()
}
setContent {
diff --git a/vending-app/src/main/kotlin/com/android/vending/extensions.kt b/vending-app/src/main/kotlin/com/android/vending/extensions.kt
index 12095facc..b0c51f94a 100644
--- a/vending-app/src/main/kotlin/com/android/vending/extensions.kt
+++ b/vending-app/src/main/kotlin/com/android/vending/extensions.kt
@@ -31,7 +31,7 @@ const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/goo
private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"
-fun buildRequestHeaders(auth: String, androidId: Long, language: List ?= null): Map {
+fun buildRequestHeaders(auth: String, androidId: Long, language: List? = null): Map {
var millis = System.currentTimeMillis()
val timestamp = TimestampContainer.Builder().container2(
TimestampContainer2.Builder().wrapper(TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build()).timestamp(makeTimestamp(millis)).build()
diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt
index 2a5dce36c..93cd8aba1 100644
--- a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt
+++ b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt
@@ -1,8 +1,6 @@
package com.android.vending.installer
import android.content.Context
-import android.content.Intent
-import kotlinx.coroutines.CompletableDeferred
import java.io.File
private const val FILE_SAVE_PATH = "phonesky-download-service"
@@ -10,7 +8,6 @@ internal const val TAG = "GmsPackageInstaller"
const val KEY_BYTES_DOWNLOADED = "bytes_downloaded"
-
fun Context.packageDownloadLocation() = File(filesDir, FILE_SAVE_PATH).apply {
if (!exists()) mkdir()
}
diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt
index de0de93cb..d25e88658 100644
--- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt
+++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt
@@ -10,18 +10,21 @@ import android.content.pm.PackageInstaller.SessionParams
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
-import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver
import kotlinx.coroutines.CompletableDeferred
import java.io.File
import java.io.FileInputStream
import java.io.IOException
@RequiresApi(Build.VERSION_CODES.M)
-public suspend fun installPackages(context: Context, callingPackage: String, componentFiles: List, isUpdate: Boolean = false, deferredMap: MutableMap> = mutableMapOf()): Intent {
+internal suspend fun Context.installPackages(
+ callingPackage: String,
+ componentFiles: List,
+ isUpdate: Boolean = false
+) {
Log.v(TAG, "installPackages start")
- val packageInstaller = context.packageManager.packageInstaller
- val installed = context.packageManager.getInstalledPackages(0).any {
+ val packageInstaller = packageManager.packageInstaller
+ val installed = packageManager.getInstalledPackages(0).any {
it.applicationInfo.packageName == callingPackage
}
// Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed.
@@ -54,14 +57,18 @@ public suspend fun installPackages(context: Context, callingPackage: String, com
totalDownloaded += file.length()
file.delete()
}
- val deferred = CompletableDeferred()
- deferredMap[sessionId] = deferred
- val intent = Intent(context, InstallResultReceiver::class.java).apply {
- putExtra(KEY_BYTES_DOWNLOADED, totalDownloaded)
- }
- val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent,
+ val deferred = CompletableDeferred()
+
+ SessionResultReceiver.pendingSessions[sessionId] = SessionResultReceiver.OnResult(
+ onSuccess = { deferred.complete(Unit) },
+ onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) }
+ )
+
+ val intent = Intent(this, SessionResultReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(this, sessionId, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
session.commit(pendingIntent.intentSender)
+
Log.d(TAG, "installPackages session commit")
return deferred.await()
} catch (e: IOException) {
diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt b/vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt
new file mode 100644
index 000000000..40ca932b2
--- /dev/null
+++ b/vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt
@@ -0,0 +1,60 @@
+package com.android.vending.installer
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import com.google.android.finsky.splitinstallservice.SplitInstallManager
+
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+internal class SessionResultReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
+ val sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
+ Log.d(TAG, "onReceive status: $status sessionId: $sessionId")
+ try {
+ when (status) {
+ PackageInstaller.STATUS_SUCCESS -> {
+ Log.d(TAG, "SessionResultReceiver received a successful transaction")
+ if (sessionId != -1) {
+ pendingSessions[sessionId]?.apply { onSuccess() }
+ pendingSessions.remove(sessionId)
+ }
+ }
+
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val extraIntent = intent.extras?.getParcelable(Intent.EXTRA_INTENT) as Intent?
+ extraIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ extraIntent?.run { ContextCompat.startActivity(context, this, null) }
+ }
+
+ else -> {
+ val errorMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
+ Log.w(TAG, "SessionResultReceiver received a failed transaction result: $errorMessage")
+ if (sessionId != -1) {
+ pendingSessions[sessionId]?.apply { onFailure(errorMessage) }
+ pendingSessions.remove(sessionId)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "SessionResultReceiver encountered error while handling session result", e)
+ if (sessionId != -1) {
+ pendingSessions[sessionId]?.apply { onFailure(e.message) }
+ }
+ }
+ }
+
+ data class OnResult(
+ val onSuccess: () -> Unit,
+ val onFailure: (message: String?) -> Unit
+ )
+
+ companion object {
+ val pendingSessions: MutableMap = mutableMapOf()
+ }
+}
\ No newline at end of file
diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt
index 11d6befb5..128cb3fe0 100644
--- a/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt
+++ b/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt
@@ -5,21 +5,31 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import androidx.annotation.RequiresApi
-import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver
-
-class Uninstaller {
-}
+import kotlinx.coroutines.CompletableDeferred
@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP)
-fun Context.uninstallPackage(packageName: String) {
+suspend fun Context.uninstallPackage(packageName: String) {
val installer = packageManager.packageInstaller
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val session = installer.createSession(sessionParams)
+
+ val deferred = CompletableDeferred()
+
+ SessionResultReceiver.pendingSessions[session] = SessionResultReceiver.OnResult(
+ onSuccess = { deferred.complete(Unit) },
+ onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) }
+ )
+
installer.uninstall(
packageName, PendingIntent.getBroadcast(
- this, session, Intent(this, InstallResultReceiver::class.java),
+ this, session, Intent(this, SessionResultReceiver::class.java).apply {
+ // for an unknown reason, the session ID is not added to the response automatically :(
+ putExtra(PackageInstaller.EXTRA_SESSION_ID, session)
+ },
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
).intentSender
)
+ deferred.await()
+
}
\ No newline at end of file
diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt
index 3be462b41..5d06d44fd 100644
--- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt
+++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt
@@ -3,5 +3,9 @@ package com.google.android.finsky.splitinstallservice
data class PackageComponent(
val packageName: String,
val componentName: String,
- val url: String
+ val url: String,
+ /**
+ * Size in bytes
+ */
+ val size: Int
)
\ No newline at end of file
diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt
index c2c595e58..d39146a5d 100644
--- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt
+++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt
@@ -4,19 +4,10 @@
*/
package com.google.android.finsky.splitinstallservice
-import android.accounts.Account
-import android.accounts.AccountManager
-import android.accounts.AuthenticatorException
-import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.content.pm.PackageInfo
-import android.content.pm.PackageInstaller
-import android.content.pm.PackageInstaller.SessionParams
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.os.Bundle
@@ -24,34 +15,21 @@ import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
-import androidx.core.content.ContextCompat
import androidx.core.content.pm.PackageInfoCompat
-import com.android.vending.AUTH_TOKEN_SCOPE
import com.android.vending.R
-import com.android.vending.buildRequestHeaders
-import com.android.vending.getAuthToken
import com.android.vending.installer.KEY_BYTES_DOWNLOADED
import com.android.vending.installer.installPackages
-import com.android.vending.installer.packageDownloadLocation
-import com.google.android.finsky.GoogleApiResponse
-import com.google.android.finsky.splitinstallservice.SplitInstallManager.Companion.deferredMap
-import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
-import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE
-import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY
+import org.microg.vending.billing.AuthManager
import org.microg.vending.billing.core.HttpClient
-import java.io.File
-import java.io.FileInputStream
-import java.io.IOException
+import org.microg.vending.delivery.downloadPackageComponents
+import org.microg.vending.delivery.requestDownloadUrls
+import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG
private const val SPLIT_INSTALL_NOTIFY_ID = 111
private const val SPLIT_INSTALL_REQUEST_TAG = "splitInstallRequestTag"
-private const val SPLIT_LANGUAGE_TAG = "config."
private const val NOTIFY_CHANNEL_ID = "splitInstall"
private const val NOTIFY_CHANNEL_NAME = "Split Install"
@@ -86,31 +64,41 @@ class SplitInstallManager(val context: Context) {
Log.v(TAG, "splitInstallFlow will query for these packages: $packagesToDownload")
if (packagesToDownload.isEmpty()) return false
- val oauthToken = runCatching { withContext(Dispatchers.IO) {
- getOauthToken()
+ val authData = runCatching { withContext(Dispatchers.IO) {
+ AuthManager.getAuthData(context)
} }.getOrNull()
- Log.v(TAG, "splitInstallFlow oauthToken: $oauthToken")
- if (oauthToken.isNullOrEmpty()) return false
+ Log.v(TAG, "splitInstallFlow oauthToken: $authData")
+ if (authData?.authToken.isNullOrEmpty()) return false
+ authData!!
notify(callingPackage)
- val components = runCatching { requestDownloadUrls(callingPackage, oauthToken, packagesToDownload) }.getOrNull()
+ val components = runCatching {
+ httpClient.requestDownloadUrls(
+ packageName = callingPackage,
+ versionCode = PackageInfoCompat.getLongVersionCode(
+ context.packageManager.getPackageInfo(callingPackage, 0)
+ ),
+ auth = authData,
+ requestSplitPackages = packagesToDownload
+ )
+ }.getOrNull()
Log.v(TAG, "splitInstallFlow requestDownloadUrls returned these components: $components")
if (components.isNullOrEmpty()) {
NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID)
return false
}
- val intent = downloadAndInstall(callingPackage, components)
+ components.forEach {
+ splitInstallRecord[it] = DownloadStatus.PENDING
+ }
- NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID)
- if (intent == null) { return false }
- sendCompleteBroad(context, callingPackage, intent)
- return true
- }
+ val packageFiles = httpClient.downloadPackageComponents(context, components, SPLIT_LANGUAGE_TAG)
+
+ packageFiles.forEach { (component, downloadFile) ->
+ splitInstallRecord[component] = if (downloadFile != null) DownloadStatus.COMPLETE else DownloadStatus.FAILED
+ }
- internal suspend fun downloadAndInstall(forPackage: String, downloadList: List, isUpdate: Boolean = false): Intent? {
- val packageFiles = downloadPackageComponents(context, downloadList)
val installFiles = packageFiles.map {
if (it.value == null) {
Log.w(TAG, "splitInstallFlow download failed, as ${it.key} was not downloaded")
@@ -119,96 +107,17 @@ class SplitInstallManager(val context: Context) {
}
Log.v(TAG, "splitInstallFlow downloaded success, downloaded ${installFiles.size} files")
- return runCatching {
- installPackages(context, forPackage, installFiles, isUpdate, deferredMap)
- }.getOrNull()
-
- }
-
- @RequiresApi(Build.VERSION_CODES.M)
- private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, requestSplitPackages: List): List {
- val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(callingPackage, 0))
- val requestUrl =
- StringBuilder("$URL_DELIVERY?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode" +
- "&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=")
- requestSplitPackages.forEach { requestUrl.append("&mn=").append(it) }
-
- Log.v(TAG, "requestDownloadUrls start")
- val languages = requestSplitPackages.filter { it.startsWith(SPLIT_LANGUAGE_TAG) }.map { it.replace(SPLIT_LANGUAGE_TAG, "") }
- Log.d(TAG, "requestDownloadUrls languages: $languages")
-
- val response = httpClient.get(
- url = requestUrl.toString(),
- headers = buildRequestHeaders(authToken, 1, languages).onEach { Log.d(TAG, "key:${it.key} value:${it.value}") },
- adapter = GoogleApiResponse.ADAPTER
- )
- Log.d(TAG, "requestDownloadUrls end response -> $response")
-
- val splitPackageResponses = response.response!!.splitReqResult!!.pkgList!!.pkgDownLoadInfo.filter {
- !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty()
- }
-
- val components: List = splitPackageResponses.mapNotNull { info ->
- requestSplitPackages.firstOrNull {
- it.contains(info.splitPkgName!!)
- }?.let {
- PackageComponent(callingPackage, it, info.downloadUrl!!)
- }
- }
-
- Log.d(TAG, "requestDownloadUrls end -> $components")
-
- components.forEach {
- splitInstallRecord[it] = DownloadStatus.PENDING
- }
-
- return components
- }
-
- @RequiresApi(Build.VERSION_CODES.M)
- private suspend fun downloadPackageComponents(
- context: Context,
- downloadList: List
- ): Map = coroutineScope {
- downloadList.map { info ->
- Log.d(TAG, "downloadSplitPackage: $info")
- async {
- info to runCatching {
- val file = File(context.packageDownloadLocation().toString(), info.componentName)
- httpClient.download(
- url = info.url,
- downloadFile = file,
- tag = SPLIT_INSTALL_REQUEST_TAG
- )
- file
- }.onFailure {
- Log.w(TAG, "downloadSplitPackage failed to downlaod from url:${info.url} to be saved as `${info.componentName}`", it)
- }.also {
- splitInstallRecord[info] = if (it.isSuccess) DownloadStatus.COMPLETE else DownloadStatus.FAILED
- }.getOrNull()
- }
- }.awaitAll().associate { it }
- }
+ val success = runCatching {
+ context.installPackages(callingPackage, installFiles, false)
+ }.isSuccess
- // TODO: use existing code
- private suspend fun getOauthToken(): String {
- val accounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE)
- var oauthToken: String? = null
- if (accounts.isEmpty()) {
- Log.w(TAG, "No Google account found")
- throw RuntimeException("No Google account found")
- } else for (account: Account in accounts) {
- oauthToken = try {
- getAuthToken(AccountManager.get(context), account, AUTH_TOKEN_SCOPE).getString(AccountManager.KEY_AUTHTOKEN)
- } catch (e: AuthenticatorException) {
- Log.w(TAG, "Could not fetch auth token for account $account")
- null
- }
- if (oauthToken != null) {
- break
- }
+ NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID)
+ return if (success) {
+ sendCompleteBroad(context, callingPackage, components.sumOf { it.size.toLong() })
+ true
+ } else {
+ false
}
- return oauthToken ?: throw RuntimeException("oauthToken is null")
}
/**
@@ -252,16 +161,15 @@ class SplitInstallManager(val context: Context) {
}
}
-
- private fun sendCompleteBroad(context: Context, packageName: String, intent: Intent) {
- Log.d(TAG, "sendCompleteBroadcast: intent:$intent")
+ private fun sendCompleteBroad(context: Context, packageName: String, bytes: Long) {
+ Log.d(TAG, "sendCompleteBroadcast: $bytes bytes")
val extra = Bundle().apply {
putInt(KEY_STATUS, 5)
putInt(KEY_ERROR_CODE, 0)
putInt(KEY_SESSION_ID, 0)
- putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0))
- putString(KEY_LANGUAGES, intent.getStringExtra(KEY_LANGUAGE))
- putLong(KEY_BYTES_DOWNLOADED, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0))
+ putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, bytes)
+ //putString(KEY_LANGUAGES, intent.getStringExtra(KEY_LANGUAGE))
+ putLong(KEY_BYTES_DOWNLOADED, bytes)
}
val broadcastIntent = Intent(ACTION_UPDATE_SERVICE).apply {
setPackage(packageName)
@@ -278,47 +186,6 @@ class SplitInstallManager(val context: Context) {
deferredMap.clear()
}
- internal class InstallResultReceiver : BroadcastReceiver() {
- @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
- override fun onReceive(context: Context, intent: Intent) {
- val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
- val sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
- Log.d(TAG, "onReceive status: $status sessionId: $sessionId")
- try {
- when (status) {
- PackageInstaller.STATUS_SUCCESS -> {
- Log.d(TAG, "InstallResultReceiver onReceive: install success")
- if (sessionId != -1) {
- deferredMap[sessionId]?.complete(intent)
- deferredMap.remove(sessionId)
- }
- }
-
- PackageInstaller.STATUS_PENDING_USER_ACTION -> {
- val extraIntent = intent.extras?.getParcelable(Intent.EXTRA_INTENT) as Intent?
- extraIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- extraIntent?.run { ContextCompat.startActivity(context, this, null) }
- }
-
- else -> {
- NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID)
- val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
- Log.w(TAG, "InstallResultReceiver onReceive: install fail -> $errorMsg")
- if (sessionId != -1) {
- deferredMap[sessionId]?.completeExceptionally(RuntimeException("install fail -> $errorMsg"))
- deferredMap.remove(sessionId)
- }
- }
- }
- } catch (e: Exception) {
- Log.w(TAG, "Error handling install result", e)
- if (sessionId != -1) {
- deferredMap[sessionId]?.completeExceptionally(e)
- }
- }
- }
- }
-
companion object {
// Installation records, including (sub)package name, download path, and installation status
internal val splitInstallRecord: MutableMap = mutableMapOf()