From 38a8148a44fb976020a32eefbca00d77b304c500 Mon Sep 17 00:00:00 2001 From: Ravi Kumar Gupta Date: Mon, 31 Aug 2026 09:18:23 +0530 Subject: [PATCH] Asset Modules: Introduce session manager with one id per download request Each startDownload allocates one ever-increasing global session id that covers all requested modules, matching Play-like PAD session semantics for #2675. --- .../assetmoduleservice/AssetModuleService.kt | 63 ++++++++++--- .../AssetModuleSessionManager.kt | 94 +++++++++++++++++++ .../finsky/assetmoduleservice/DownloadData.kt | 11 ++- .../com/google/android/finsky/extensions.kt | 93 +++++++++++------- 4 files changed, 208 insertions(+), 53 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleSessionManager.kt diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt index d8bb72eb2c..d7af992c0e 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleService.kt @@ -57,6 +57,7 @@ class AssetModuleServiceImpl( ) : AbstractAssetModuleServiceImpl(context, lifecycle) { private val fileDescriptorMap = mutableMapOf() private val lock = Any() + private val sessionManager = AssetModuleSessionManager(context) private fun checkSessionValid(packageName: String, sessionId: Int) { Log.d(TAG, "checkSessionValid: $packageName $sessionId ${packageDownloadData[packageName]?.sessionIds}") @@ -92,6 +93,21 @@ class AssetModuleServiceImpl( Log.d(TAG, "startDownload: resetAllModuleStatus ") packageDownloadData[packageName]?.resetAllModuleStatus() } + + params.moduleNames.forEach { moduleName -> + val status = synchronized(lock) { + packageDownloadData[packageName]?.getModuleData(moduleName)?.status + } + if (status == AssetPackStatus.DOWNLOADING) { + Log.w(TAG, "startDownload: module $moduleName still DOWNLOADING; overlapping cancel deferred to #2673") + } + } + + val session = sessionManager.createSession(packageName, params.moduleNames) + synchronized(lock) { + packageDownloadData[packageName]?.bindModulesToSession(context, session.sessionId, params.moduleNames) + } + params.moduleNames.forEach { val moduleData = packageDownloadData[packageName]?.getModuleData(it) if (moduleData?.status != AssetPackStatus.DOWNLOADING && moduleData?.status != AssetPackStatus.COMPLETED) { @@ -110,8 +126,8 @@ class AssetModuleServiceImpl( } } val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, params.moduleNames) - Log.d(TAG, "startDownload: $bundleData") - callback?.onStartDownload(-1, bundleData) + Log.d(TAG, "startDownload: sessionId=${session.sessionId} $bundleData") + callback?.onStartDownload(session.sessionId, bundleData) params.moduleNames.forEach { val packData = packageDownloadData[packageName]?.getModuleData(it) if (packData?.status == AssetPackStatus.PENDING) { @@ -134,15 +150,24 @@ class AssetModuleServiceImpl( return } - packageDownloadData[packageName]?.moduleNames?.forEach { moduleName -> - if (moduleName in params.installedAssetModules) return@forEach - - listBundleData.add(sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, null, null)) + val downloadData = packageDownloadData[packageName] + if (downloadData != null) { + val modulesBySession = downloadData.moduleNames + .filter { it !in params.installedAssetModules } + .groupBy { downloadData.sessionIds[it] ?: downloadData.sessionId } - packageDownloadData[packageName]?.getModuleData(moduleName)?.chunks?.forEach { chunkData -> - val destination = chunkData.getChunkFile(context) - if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { - sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, moduleName, chunkData, destination) + modulesBySession.forEach { (sessionId, modules) -> + if (sessionId == 0 && modules.all { downloadData.getModuleData(it).status == AssetPackStatus.NOT_INSTALLED }) { + return@forEach + } + listBundleData.add(buildDownloadBundle(downloadData, modules)) + modules.forEach { moduleName -> + downloadData.getModuleData(moduleName).chunks.forEach { chunkData -> + val destination = chunkData.getChunkFile(context) + if (destination.exists() && destination.length() == chunkData.chunkBytesToDownload) { + sendBroadcastForExistingFile(context, downloadData, moduleName, chunkData, destination) + } + } } } } @@ -176,6 +201,7 @@ class AssetModuleServiceImpl( synchronized(lock) { packageDownloadData[packageName]?.updateDownloadStatus(params.moduleName, AssetPackStatus.COMPLETED) sendBroadcastForExistingFile(context, packageDownloadData[packageName]!!, params.moduleName, null, null) + packageDownloadData[packageName]?.let { sessionManager.markTerminalIfDone(params.sessionId, it) } } val directory = context.getModuleDir(params.sessionId, params.moduleName) @@ -195,9 +221,19 @@ class AssetModuleServiceImpl( override suspend fun notifySessionFailed(params: NotifySessionFailedParameters, packageName: String, callback: IAssetModuleServiceCallback?) { checkSessionValid(packageName, params.sessionId) - // TODO: Implement + synchronized(lock) { + val downloadData = packageDownloadData[packageName] + if (downloadData != null) { + val modulesInSession = downloadData.sessionIds.filter { it.value == params.sessionId }.keys + modulesInSession.forEach { moduleName -> + downloadData.updateDownloadStatus(moduleName, AssetPackStatus.FAILED) + sendBroadcastForExistingFile(context, downloadData, moduleName, null, null) + } + sessionManager.markTerminalIfDone(params.sessionId, downloadData) + } + } + // Full cancel of in-flight downloads is #2673 callback?.onNotifySessionFailed(bundleOf(BundleKeys.SESSION_ID to params.sessionId)) - //throw UnsupportedOperationException() } override suspend fun keepAlive(params: KeepAliveParameters, packageName: String, callback: IAssetModuleServiceCallback?) { @@ -251,6 +287,7 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NETWORK_ERROR) }) } } + // Do not mint a new session here — only startDownload allocates session ids. val bundleData = buildDownloadBundle(packageDownloadData[packageName]!!, params.moduleNames) Log.d(TAG, "requestDownloadInfo -> $bundleData") callback?.onRequestDownloadInfo(bundleData, bundleData) @@ -267,4 +304,4 @@ class AssetModuleServiceImpl( callback?.onCancelDownloads(bundleOf(BundleKeys.ERROR_CODE to AssetPackErrorCode.NO_ERROR)) //throw UnsupportedOperationException() } -} \ No newline at end of file +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleSessionManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleSessionManager.kt new file mode 100644 index 0000000000..72b013fd08 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/AssetModuleSessionManager.kt @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.assetmoduleservice + +import android.content.Context +import android.os.SystemClock +import android.util.Log +import com.google.android.play.core.assetpacks.model.AssetPackStatus + +private const val SESSION_MGR_TAG = "AssetModuleSessionMgr" +private const val PREFS_NAME = "AssetModuleSessionIdGenerator" +private const val PREFS_KEY_LATEST = "Latest" + +data class AssetModuleSession( + val sessionId: Int, + val packageName: String, + val moduleNames: Set, + val createdAtElapsedMs: Long, + var terminal: Boolean = false +) + +/** + * Play-like session registry for asset module downloads. + * One ever-increasing session id covers all modules in a single startDownload request. + */ +class AssetModuleSessionManager(private val context: Context) { + private val lock = Any() + private val sessionsById = mutableMapOf() + private val sessionIdsByPackage = mutableMapOf>() + + fun nextSessionId(): Int = synchronized(lock) { + allocateSessionIdLocked() + } + + fun createSession(packageName: String, modules: Collection): AssetModuleSession { + synchronized(lock) { + val sessionId = allocateSessionIdLocked() + val session = AssetModuleSession( + sessionId = sessionId, + packageName = packageName, + moduleNames = modules.toSet(), + createdAtElapsedMs = SystemClock.elapsedRealtime() + ) + sessionsById[sessionId] = session + sessionIdsByPackage.getOrPut(packageName) { mutableSetOf() }.add(sessionId) + Log.d(SESSION_MGR_TAG, "createSession: package=$packageName sessionId=$sessionId modules=${session.moduleNames}") + return session + } + } + + fun getSession(sessionId: Int): AssetModuleSession? = synchronized(lock) { + sessionsById[sessionId] + } + + fun findActiveSession(packageName: String, module: String): AssetModuleSession? = synchronized(lock) { + val ids = sessionIdsByPackage[packageName] ?: return null + ids.mapNotNull { sessionsById[it] } + .filter { !it.terminal && module in it.moduleNames } + .maxByOrNull { it.createdAtElapsedMs } + } + + fun sessionsForPackage(packageName: String): List = synchronized(lock) { + sessionIdsByPackage[packageName] + ?.mapNotNull { sessionsById[it] } + ?.sortedBy { it.createdAtElapsedMs } + ?: emptyList() + } + + fun markTerminalIfDone(sessionId: Int, downloadData: DownloadData) { + synchronized(lock) { + val session = sessionsById[sessionId] ?: return + val allTerminal = session.moduleNames.all { moduleName -> + val status = runCatching { downloadData.getModuleData(moduleName).status }.getOrNull() + status == AssetPackStatus.COMPLETED || + status == AssetPackStatus.FAILED || + status == AssetPackStatus.CANCELED + } + if (allTerminal) { + session.terminal = true + Log.d(SESSION_MGR_TAG, "markTerminalIfDone: sessionId=$sessionId terminal=true") + } + } + } + + private fun allocateSessionIdLocked(): Int { + val sharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val latest = sharedPreferences.getInt(PREFS_KEY_LATEST, 0) + 1 + sharedPreferences.edit().putInt(PREFS_KEY_LATEST, latest).commit() + return latest + } +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt index 6ad18ab8cc..5a78e853d4 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/DownloadData.kt @@ -6,16 +6,15 @@ package com.google.android.finsky.assetmoduleservice import android.content.Context -import android.util.Log import com.google.android.finsky.getChunkFile import com.google.android.play.core.assetpacks.model.AssetPackStatus import com.google.android.play.core.assetpacks.protocol.CompressionFormat -import java.io.File import java.io.Serializable data class DownloadData( var packageName: String = "", var errorCode: Int = 0, + var sessionId: Int = 0, var sessionIds: Map = emptyMap(), var status: Int = 0, var moduleNames: Set = emptySet(), @@ -48,7 +47,11 @@ fun DownloadData?.merge(data: DownloadData?): DownloadData? { if (this == null) return data if (data == null) return this moduleNames += data.moduleNames - sessionIds += data.sessionIds.filter { it.key !in sessionIds.keys } + // Overwrite session mapping for modules present in incoming data (new startDownload session) + sessionIds = sessionIds + data.sessionIds + if (data.sessionId != 0) { + sessionId = data.sessionId + } moduleDataMap += data.moduleDataMap.filter { it.key !in moduleDataMap.keys } return this } @@ -85,4 +88,4 @@ data class ChunkData( val numberOfChunksInSlice: Int ) -fun ChunkData.getChunkFile(context: Context) = context.getChunkFile(sessionId, moduleName, sliceId, chunkIndex) \ No newline at end of file +fun ChunkData.getChunkFile(context: Context) = context.getChunkFile(sessionId, moduleName, sliceId, chunkIndex) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt index a3d44ae147..6e014e76d4 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/extensions.kt @@ -151,33 +151,10 @@ suspend fun syncDeviceInfo(context: Context, account: Account, authToken: String } } -private val sessionIdMap: MutableMap = mutableMapOf() - -private val lock = Any() -private fun Context.generateSessionId(): Int { - synchronized(lock) { - val sharedPreferences = getSharedPreferences("AssetModuleSessionIdGenerator", 0) - val latest = sharedPreferences.getInt("Latest", 0) + 1 - val edit = sharedPreferences.edit() - edit.putInt("Latest", latest) - edit.commit() - return latest - } -} - -private fun getSessionIdForPackage(packageName: String): Int { - synchronized(lock) { - return sessionIdMap.getOrPut(packageName) { 10 } - } -} - -private fun updateSessionIdForPackage(packageName: String, increment: Int) { - synchronized(lock) { - val currentSessionId = sessionIdMap[packageName] ?: 10 - sessionIdMap[packageName] = currentSessionId + increment - } -} - +/** + * Metadata-only init: does not allocate a download session id. + * [bindModulesToSession] assigns one shared id on startDownload. + */ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, deliveryInfo: ModuleDeliveryInfo?): DownloadData? { if (deliveryInfo == null || deliveryInfo.status != null) { return null @@ -187,7 +164,6 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d var packVersionCode = 0L val sessionIds = arrayMapOf() val moduleDataMap = arrayMapOf() - val baseSessionId = getSessionIdForPackage(packageName) for (moduleIndex in deliveryInfo.modules.indices) { val moduleInfo: ModuleInfo = deliveryInfo.modules[moduleIndex] packVersionCode = moduleInfo.packVersion ?: 0 @@ -195,7 +171,8 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d val moduleName: String = moduleInfo.moduleName ?: continue var moduleBytesToDownload = 0L moduleNames.add(moduleName) - sessionIds[moduleName] = baseSessionId + moduleIndex + // Placeholder until bindModulesToSession; requestDownloadInfo must not mint ids. + sessionIds[moduleName] = 0 var totalSumOfSubcontractedModules = 0 val sliceIds: ArrayList = ArrayList() val chunkDatas: ArrayList = arrayListOf() @@ -215,7 +192,7 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d sliceBytesToDownload += dResource.bytesToDownload!! totalSumOfSubcontractedModules += 1 chunkDatas.add(ChunkData( - sessionId = sessionIds[moduleName]!!, + sessionId = 0, moduleName = moduleName, sliceId = sliceId, chunkSourceUri = dResource.sourceUri, @@ -242,30 +219,73 @@ private fun initModuleDownloadInfo(packageName: String, appVersionCode: Long?, d totalBytesToDownload += moduleBytesToDownload moduleDataMap[moduleName] = moduleData } - updateSessionIdForPackage(packageName, deliveryInfo.modules.size) return DownloadData( packageName = packageName, errorCode = AssetPackErrorCode.NO_ERROR, + sessionId = 0, sessionIds = sessionIds, status = AssetPackStatus.NOT_INSTALLED, moduleNames = moduleNames, appVersionCode = appVersionCode ?: packVersionCode, - moduleDataMap + moduleDataMap = moduleDataMap ) } +/** + * Bind [moduleNames] to one shared [sessionId], rewriting chunk metadata and renaming + * existing on-disk module dirs into the new session path when possible. + */ +fun DownloadData.bindModulesToSession(context: Context, sessionId: Int, moduleNames: Collection) { + this.sessionId = sessionId + val updatedSessionIds = sessionIds.toMutableMap() + for (moduleName in moduleNames) { + val previousSessionId = updatedSessionIds[moduleName] ?: 0 + if (previousSessionId != 0 && previousSessionId != sessionId) { + val moved = context.rebindModuleDirToSession(previousSessionId, sessionId, moduleName) + if (!moved) { + Log.d(TAG, "bindModulesToSession: no existing dir for $moduleName under session $previousSessionId; will re-download") + } + } + updatedSessionIds[moduleName] = sessionId + val moduleData = moduleDataMap[moduleName] ?: continue + moduleData.chunks = moduleData.chunks.map { it.copy(sessionId = sessionId) } + } + sessionIds = updatedSessionIds +} + +fun Context.rebindModuleDirToSession(oldSessionId: Int, newSessionId: Int, moduleName: String): Boolean { + if (oldSessionId == newSessionId) return true + val oldDir = getModuleDir(oldSessionId, moduleName) + val newDir = getModuleDir(newSessionId, moduleName) + if (newDir.exists()) return true + if (!oldDir.exists()) return false + newDir.parentFile?.mkdirs() + val renamed = oldDir.renameTo(newDir) + if (!renamed) { + Log.w(TAG, "rebindModuleDirToSession: failed to rename $oldDir -> $newDir") + } + return renamed +} + fun buildDownloadBundle(downloadData: DownloadData, list: List? = null): Bundle { val bundleData = Bundle() val arrayList = arrayListOf() var totalBytesToDownload = 0L var bytesDownloaded = 0L + val sharedSessionId = downloadData.sessionId.takeIf { it != 0 } + ?: list?.firstNotNullOfOrNull { downloadData.sessionIds[it]?.takeIf { id -> id != 0 } } + ?: 0 + + if (sharedSessionId != 0) { + bundleData.put(BundleKeys.SESSION_ID, sharedSessionId) + } list?.forEach { moduleName -> val packData = downloadData.getModuleData(moduleName) + val moduleSessionId = downloadData.sessionIds[moduleName]?.takeIf { it != 0 } ?: sharedSessionId bundleData.put(BundleKeys.STATUS, packData.status) - downloadData.sessionIds[moduleName]?.let { sessionId -> - bundleData.put(BundleKeys.SESSION_ID, sessionId) - bundleData.put(BundleKeys.SESSION_ID, moduleName, packData.status) + if (moduleSessionId != 0) { + bundleData.put(BundleKeys.SESSION_ID, moduleName, moduleSessionId) } bundleData.put(BundleKeys.PACK_VERSION_TAG, moduleName, null) bundleData.put(BundleKeys.STATUS, moduleName, packData.status) @@ -291,7 +311,8 @@ fun sendBroadcastForExistingFile(context: Context, downloadData: DownloadData, m val downloadBundle = Bundle() downloadBundle.put(BundleKeys.APP_VERSION_CODE, downloadData.appVersionCode.toInt()) downloadBundle.put(BundleKeys.ERROR_CODE, AssetPackErrorCode.NO_ERROR) - downloadBundle.put(BundleKeys.SESSION_ID, downloadData.sessionIds[moduleName] ?: downloadData.status) + val sessionId = downloadData.sessionIds[moduleName]?.takeIf { it != 0 } ?: downloadData.sessionId + downloadBundle.put(BundleKeys.SESSION_ID, sessionId) downloadBundle.put(BundleKeys.STATUS, packData.status) downloadBundle.put(BundleKeys.PACK_NAMES, arrayListOf(moduleName)) downloadBundle.put(BundleKeys.BYTES_DOWNLOADED, packData.bytesDownloaded)