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()