From 48a3eeea643f53e3c06c568639275c4554c91766 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Sat, 12 Oct 2024 09:40:10 +0800 Subject: [PATCH] Change the Volley download request process to HttpURLConnection. It is necessary to send progress change broadcasts in real time, otherwise the progress bar will be stuck. --- .../microg/vending/billing/core/HttpClient.kt | 40 +--- .../assetmoduleservice/AssetModuleService.kt | 200 +++++++++--------- .../finsky/assetmoduleservice/ModuleData.kt | 25 ++- .../com/google/android/finsky/extensions.kt | 157 ++++++++++---- 4 files changed, 235 insertions(+), 187 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt index 49d849312..3d66ae166 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt @@ -2,7 +2,11 @@ package org.microg.vending.billing.core import android.content.Context import android.net.Uri -import com.android.volley.* +import com.android.volley.DefaultRetryPolicy +import com.android.volley.NetworkResponse +import com.android.volley.Request +import com.android.volley.Response +import com.android.volley.VolleyError import com.android.volley.toolbox.HttpHeaderParser import com.android.volley.toolbox.JsonObjectRequest import com.android.volley.toolbox.Volley @@ -10,9 +14,6 @@ import com.squareup.wire.Message import com.squareup.wire.ProtoAdapter import org.json.JSONObject import org.microg.gms.utils.singleInstanceOf -import java.io.File -import java.io.FileOutputStream -import java.io.IOException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -23,37 +24,6 @@ class HttpClient(context: Context) { val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } - suspend fun download( - url: String, downloadFile: File, tag: String - ): 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 { - if (response.statusCode != 200) throw VolleyError(response) - return try { - val parentDir = downloadFile.getParentFile() - if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { - throw IOException("Failed to create directories: ${parentDir.absolutePath}") - } - val fos = FileOutputStream(downloadFile) - fos.write(response.data) - fos.close() - Response.success(downloadFile.absolutePath, HttpHeaderParser.parseCacheHeaders(response)) - } catch (e: Exception) { - Response.error(VolleyError(e)) - } - } - - override fun deliverResponse(response: String) { - continuation.resume(response) - } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) - } - }.setShouldCache(false).setTag(tag)) - } - suspend fun get( url: String, headers: Map = emptyMap(), 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 9157756eb..39cd1245d 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 @@ -4,7 +4,6 @@ */ package com.google.android.finsky.assetmoduleservice -import android.accounts.Account import android.accounts.AccountManager import android.content.Context import android.content.Intent @@ -17,39 +16,33 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope -import com.android.vending.licensing.AUTH_TOKEN_SCOPE -import com.android.vending.licensing.getAuthToken -import com.google.android.finsky.AssetModuleDeliveryRequest -import com.google.android.finsky.AssetModuleInfo -import com.google.android.finsky.CallerInfo -import com.google.android.finsky.CallerState +import com.google.android.finsky.ERROR_CODE_FAIL +import com.google.android.finsky.KEY_APP_VERSION_CODE +import com.google.android.finsky.KEY_BYTES_DOWNLOADED import com.google.android.finsky.KEY_CHUNK_FILE_DESCRIPTOR import com.google.android.finsky.KEY_CHUNK_NUMBER import com.google.android.finsky.KEY_ERROR_CODE import com.google.android.finsky.KEY_MODULE_NAME +import com.google.android.finsky.KEY_PACK_BASE_VERSION import com.google.android.finsky.KEY_PACK_NAMES +import com.google.android.finsky.KEY_PACK_VERSION import com.google.android.finsky.KEY_PLAY_CORE_VERSION_CODE import com.google.android.finsky.KEY_RESOURCE_PACKAGE_NAME import com.google.android.finsky.KEY_SESSION_ID import com.google.android.finsky.KEY_SLICE_ID -import com.google.android.finsky.PageSource +import com.google.android.finsky.KEY_STATUS +import com.google.android.finsky.KEY_TOTAL_BYTES_TO_DOWNLOAD import com.google.android.finsky.STATUS_COMPLETED import com.google.android.finsky.STATUS_DOWNLOADING -import com.google.android.finsky.STATUS_NOT_INSTALLED -import com.google.android.finsky.STATUS_TRANSFERRING +import com.google.android.finsky.STATUS_INITIAL_STATE import com.google.android.finsky.TAG_REQUEST import com.google.android.finsky.buildDownloadBundle +import com.google.android.finsky.combineModule import com.google.android.finsky.downloadFile -import com.google.android.finsky.getAppVersionCode -import com.google.android.finsky.initModuleDownloadInfo -import com.google.android.finsky.requestAssetModule +import com.google.android.finsky.initAssertModuleData import com.google.android.finsky.sendBroadcastForExistingFile import com.google.android.play.core.assetpacks.protocol.IAssetModuleService import com.google.android.play.core.assetpacks.protocol.IAssetModuleServiceCallback -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collectLatest -import okhttp3.internal.filterList -import org.microg.gms.auth.AuthConstants import org.microg.gms.profile.ProfileManager import org.microg.vending.billing.core.HttpClient import java.io.File @@ -80,8 +73,9 @@ class AssetModuleService : LifecycleService() { class AssetModuleServiceImpl( val context: Context, override val lifecycle: Lifecycle, private val httpClient: HttpClient, private val accountManager: AccountManager ) : IAssetModuleService.Stub(), LifecycleOwner { - private val sharedModuleDataFlow = MutableSharedFlow() + @Volatile private var moduleData: ModuleData? = null + private val fileDescriptorMap = mutableMapOf() override fun startDownload(packageName: String?, list: MutableList?, bundle: Bundle?, callback: IAssetModuleServiceCallback?) { Log.d(TAG, "Method (startDownload) called by packageName -> $packageName") @@ -90,38 +84,63 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - if (moduleData == null && moduleErrorRequested.contains(packageName)) { - Log.d(TAG, "startDownload: moduleData request error") - val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } - callback?.onStartDownload(-1, result) - return - } - suspend fun prepare(data: ModuleData) { - list.forEach { - val moduleName = it.getString(KEY_MODULE_NAME) - if (moduleName != null) { - callback?.onStartDownload(-1, buildDownloadBundle(moduleName, data, true)) - val packData = data.getPackData(moduleName) - if (packData?.status != STATUS_NOT_INSTALLED) { - Log.w(TAG, "startDownload: packData?.status is ${packData?.status}") - return@forEach - } - data.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) - data.updateModuleDownloadStatus(STATUS_DOWNLOADING) - packData.bundleList?.forEach { download -> - if (moduleName == download.getString(KEY_RESOURCE_PACKAGE_NAME)) { - httpClient.downloadFile(context, moduleName, data, download) + lifecycleScope.launchWhenStarted { + if (moduleErrorRequested.contains(packageName)) { + Log.d(TAG, "startDownload: moduleData request error") + val result = Bundle().apply { putStringArrayList(KEY_PACK_NAMES, arrayListOf()) } + Log.d(TAG, "prepare: ${result.keySet()}") + callback?.onStartDownload(-1, result) + return@launchWhenStarted + } + if (moduleData?.status != STATUS_DOWNLOADING) { + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (moduleData == null) { + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + moduleData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) + } + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + if (moduleName != null) { + val packData = moduleData!!.getPackData(moduleName) + callback?.onStartDownload(-1, buildDownloadBundle(moduleName, moduleData!!, true)) + if (packData?.status != STATUS_INITIAL_STATE) { + Log.w(TAG, "startDownload: packData?.status is ${packData?.status}") + return@forEach + } + moduleData?.updateDownloadStatus(moduleName, STATUS_DOWNLOADING) + moduleData?.updateModuleDownloadStatus(STATUS_DOWNLOADING) + packData.bundleList?.forEach { download -> + if (moduleName == download.getString(KEY_RESOURCE_PACKAGE_NAME)) { + downloadFile(context, moduleName, moduleData!!, download) + } } } } - } - } - lifecycleScope.launchWhenStarted { - if (moduleData == null) { - sharedModuleDataFlow.collectLatest { prepare(it) } return@launchWhenStarted } - prepare(moduleData!!) + val result = Bundle() + val arrayList = arrayListOf() + result.putInt(KEY_STATUS, moduleData!!.status) + result.putLong(KEY_APP_VERSION_CODE, moduleData!!.appVersionCode) + result.putLong(KEY_BYTES_DOWNLOADED, moduleData!!.bytesDownloaded) + result.putInt(KEY_ERROR_CODE, moduleData!!.errorCode) + result.putInt(KEY_SESSION_ID, 6) + result.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleData!!.totalBytesToDownload) + list.forEach { + val moduleName = it.getString(KEY_MODULE_NAME) + arrayList.add(moduleName!!) + val packData = moduleData!!.getPackData(moduleName) + result.putInt(combineModule(KEY_SESSION_ID, moduleName), packData!!.sessionId) + result.putInt(combineModule(KEY_STATUS, moduleName), packData.status) + result.putInt(combineModule(KEY_ERROR_CODE, moduleName), packData.errorCode) + result.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) + result.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) + result.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) + result.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) + sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) + } + result.putStringArrayList(KEY_PACK_NAMES, arrayList) + callback?.onStartDownload(-1, result) } } @@ -130,8 +149,20 @@ class AssetModuleServiceImpl( } override fun notifyChunkTransferred(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { - Log.d(TAG, "Method (notifyChunkTransferred) called but not implemented by packageName -> $packageName") - callback?.onNotifyChunkTransferred(bundle, Bundle()) + Log.d(TAG, "Method (notifyChunkTransferred) called by packageName -> $packageName") + val moduleName = bundle?.getString(KEY_MODULE_NAME) + if (moduleName.isNullOrEmpty()) { + Log.d(TAG, "notifyChunkTransferred: params invalid ") + callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) + return + } + val sessionId = bundle.getInt(KEY_SESSION_ID) + val sliceId = bundle.getString(KEY_SLICE_ID) + val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) + val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" + fileDescriptorMap[downLoadFile]?.close() + fileDescriptorMap.remove(downLoadFile) + callback?.onNotifyChunkTransferred(bundle, Bundle().apply { putInt(KEY_ERROR_CODE, 0) }) } override fun notifyModuleCompleted(packageName: String?, bundle: Bundle?, bundle2: Bundle?, callback: IAssetModuleServiceCallback?) { @@ -142,21 +173,16 @@ class AssetModuleServiceImpl( callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return } - fun notify(data: ModuleData) { - callback?.onNotifyModuleCompleted(bundle, bundle) - val notify = data.packNames?.all { data.getPackData(it)?.status == STATUS_TRANSFERRING } ?: false - if (notify) { - data.packNames?.forEach { moduleData?.updateDownloadStatus(it, STATUS_COMPLETED) } - data.updateModuleDownloadStatus(STATUS_COMPLETED) - sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) - } - } lifecycleScope.launchWhenStarted { - if (moduleData == null) { - sharedModuleDataFlow.collectLatest { notify(it) } - return@launchWhenStarted + Log.d(TAG, "notify: moduleName: $moduleName packNames: ${moduleData?.packNames}") + moduleData?.packNames?.find { it == moduleName }?.let { + moduleData?.updateDownloadStatus(it, STATUS_COMPLETED) + if (moduleData?.packNames?.all { pack -> moduleData?.getPackData(pack)?.status == STATUS_COMPLETED } == true) { + moduleData?.updateModuleDownloadStatus(STATUS_COMPLETED) + } + sendBroadcastForExistingFile(context, moduleData!!, moduleName, null, null) } - notify(moduleData!!) + callback?.onNotifyModuleCompleted(bundle, bundle) } } @@ -169,7 +195,7 @@ class AssetModuleServiceImpl( } override fun getChunkFileDescriptor(packageName: String, bundle: Bundle, bundle2: Bundle, callback: IAssetModuleServiceCallback?) { - Log.d(TAG, "Method (getChunkFileDescriptor) called but not implemented by packageName -> $packageName") + Log.d(TAG, "Method (getChunkFileDescriptor) called by packageName -> $packageName") val moduleName = bundle.getString(KEY_MODULE_NAME) if (moduleName.isNullOrEmpty()) { Log.d(TAG, "getChunkFileDescriptor: params invalid ") @@ -182,7 +208,9 @@ class AssetModuleServiceImpl( val chunkNumber = bundle.getInt(KEY_CHUNK_NUMBER) val downLoadFile = "${context.filesDir.absolutePath}/assetpacks/$sessionId/$moduleName/$sliceId/$chunkNumber" val filePath = Uri.parse(downLoadFile).path?.let { File(it) } - ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY) + ParcelFileDescriptor.open(filePath, ParcelFileDescriptor.MODE_READ_ONLY).also { + fileDescriptorMap[downLoadFile] = it + } }.getOrNull() callback?.onGetChunkFileDescriptor(Bundle().apply { putParcelable(KEY_CHUNK_FILE_DESCRIPTOR, parcelFileDescriptor) }, Bundle()) } @@ -195,45 +223,12 @@ class AssetModuleServiceImpl( return } lifecycleScope.launchWhenStarted { - val requestedAssetModuleNames = arrayListOf() - for (data in list) { - val moduleName = data.getString(KEY_MODULE_NAME) - if (!moduleName.isNullOrEmpty()) { - requestedAssetModuleNames.add(moduleName) - } - } - if (!moduleData?.packNames.isNullOrEmpty()) { - val bundleData = Bundle().apply { - val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size - requestedAssetModuleNames.forEach { - putAll(buildDownloadBundle(it, moduleData!!, isPack, packNames = requestedAssetModuleNames)) - } - } - callback?.onRequestDownloadInfo(bundleData, bundleData) - return@launchWhenStarted - } - val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) - var oauthToken: String? = null - if (accounts.isEmpty()) { - callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) - return@launchWhenStarted - } else for (account: Account in accounts) { - oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) - if (oauthToken != null) { - break - } + val requestedAssetModuleNames = list.map { it.getString(KEY_MODULE_NAME) }.filter { !it.isNullOrEmpty() } + if (moduleData == null) { + val playCoreVersionCode = bundle.getInt(KEY_PLAY_CORE_VERSION_CODE) + moduleData = httpClient.initAssertModuleData(context, packageName, accountManager, requestedAssetModuleNames, playCoreVersionCode) } - val requestPayload = - AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) - .playCoreVersion(bundle.getInt(KEY_PLAY_CORE_VERSION_CODE)) - .pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) - .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { - list.filterList { !getString(KEY_MODULE_NAME).isNullOrEmpty() }.forEach { - add(AssetModuleInfo.Builder().name(it.getString(KEY_MODULE_NAME)).build()) - } - }).build() - val moduleDeliveryInfo = httpClient.requestAssetModule(context, oauthToken!!, requestPayload) - if (moduleDeliveryInfo == null || moduleDeliveryInfo.status != null) { + if (moduleData?.errorCode == ERROR_CODE_FAIL) { if (moduleErrorRequested.contains(packageName)) { callback?.onError(Bundle().apply { putInt(KEY_ERROR_CODE, -5) }) return@launchWhenStarted @@ -244,16 +239,13 @@ class AssetModuleServiceImpl( return@launchWhenStarted } moduleErrorRequested.remove(packageName) - Log.d(TAG, "requestDownloadInfo: moduleDeliveryInfo-> $moduleDeliveryInfo") - moduleData = initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) val bundleData = Bundle().apply { val isPack = moduleData?.packNames?.size != requestedAssetModuleNames.size requestedAssetModuleNames.forEach { - putAll(buildDownloadBundle(it, moduleData!!, isPack, packNames = requestedAssetModuleNames)) + putAll(buildDownloadBundle(it!!, moduleData!!, isPack, packNames = requestedAssetModuleNames)) } } callback?.onRequestDownloadInfo(bundleData, bundleData) - sharedModuleDataFlow.emit(moduleData!!) } } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt index 064fb20c9..e52919b5d 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/assetmoduleservice/ModuleData.kt @@ -5,8 +5,10 @@ package com.google.android.finsky.assetmoduleservice +import android.content.Context import android.os.Bundle import androidx.collection.ArrayMap +import com.google.android.finsky.sendBroadcastForExistingFile data class ModuleData( var packageName: String? = null, @@ -15,8 +17,8 @@ data class ModuleData( var bytesDownloaded: Long = 0, var status: Int = 0, var packNames: ArrayList? = null, - var appVersionCode: Int = 0, - var totalBytesToDownload: Long = 0 + var appVersionCode: Long = 0, + var totalBytesToDownload: Long = 0, ) { private var mPackData = emptyMap() @@ -28,12 +30,9 @@ data class ModuleData( return mPackData[packName] } - fun incrementPackBytesDownloaded(packName: String, bytes: Long) { + fun incrementPackBytesDownloaded(context: Context, packName: String, bytes: Long) { mPackData[packName]?.incrementBytesDownloaded(bytes) - } - - fun incrementBytesDownloaded(packName: String) { - bytesDownloaded += getPackData(packName)?.bytesDownloaded ?: 0 + bytesDownloaded += bytes } fun updateDownloadStatus(packName: String, statusCode: Int) { @@ -44,11 +43,15 @@ data class ModuleData( fun updateModuleDownloadStatus(statusCode: Int) { this.status = statusCode } + + override fun toString(): String { + return "ModuleData(packageName=$packageName, errorCode=$errorCode, sessionIds=$sessionIds, bytesDownloaded=$bytesDownloaded, status=$status, packNames=$packNames, appVersionCode=$appVersionCode, totalBytesToDownload=$totalBytesToDownload, mPackData=$mPackData)" + } } data class PackData( - var packVersion: Int = 0, - var packBaseVersion: Int = 0, + var packVersion: Long = 0, + var packBaseVersion: Long = 0, var sessionId: Int = 0, var errorCode: Int = 0, var status: Int = 0, @@ -63,4 +66,8 @@ data class PackData( fun incrementBytesDownloaded(bytes: Long) { bytesDownloaded += bytes } + + override fun toString(): String { + return "PackData(packVersion=$packVersion, packBaseVersion=$packBaseVersion, sessionId=$sessionId, errorCode=$errorCode, status=$status, bytesDownloaded=$bytesDownloaded, totalBytesToDownload=$totalBytesToDownload, packVersionTag=$packVersionTag, bundleList=$bundleList, totalSumOfSubcontractedModules=$totalSumOfSubcontractedModules, subcontractingBaseUnit=$subcontractingBaseUnit, listOfSubcontractNames=$listOfSubcontractNames)" + } } 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 2aa968763..a1511cce9 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 @@ -5,27 +5,38 @@ package com.google.android.finsky +import android.accounts.Account +import android.accounts.AccountManager import android.content.Context import android.content.Intent -import android.database.Cursor import android.net.Uri import android.os.Bundle import android.util.Log import androidx.collection.arrayMapOf import androidx.collection.arraySetOf +import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.licensing.getAuthToken import com.android.vending.licensing.getLicenseRequestHeaders import com.google.android.finsky.assetmoduleservice.ModuleData import com.google.android.finsky.assetmoduleservice.PackData -import org.microg.gms.settings.SettingsContract +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.microg.gms.auth.AuthConstants import org.microg.vending.billing.core.HttpClient import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL import java.util.Collections const val STATUS_NOT_INSTALLED = 8 const val STATUS_COMPLETED = 4 -const val STATUS_TRANSFERRING = 3 const val STATUS_DOWNLOADING = 2 -const val STATUS_SUCCESS = 0 +const val STATUS_INITIAL_STATE = 1 + +const val ERROR_CODE_SUCCESS = 0 +const val ERROR_CODE_FAIL = -5 const val KEY_ERROR_CODE = "error_code" const val KEY_MODULE_NAME = "module_name" @@ -78,21 +89,47 @@ fun getAppVersionCode(context: Context, packageName: String): String? { return runCatching { context.packageManager.getPackageInfo(packageName, 0).versionCode.toString() }.getOrNull() } -suspend fun HttpClient.requestAssetModule(context: Context, auth: String, requestPayload: AssetModuleDeliveryRequest) = runCatching { - val androidId = SettingsContract.getSettings( - context, SettingsContract.CheckIn.getContentUri(context), arrayOf(SettingsContract.CheckIn.ANDROID_ID) - ) { cursor: Cursor -> cursor.getLong(0) } - Log.d(TAG, "auth->$auth") - Log.d(TAG, "androidId->$androidId") - Log.d(TAG, "requestPayload->$requestPayload") - post( - url = ASSET_MODULE_DELIVERY_URL, headers = getLicenseRequestHeaders(auth, 1), payload = requestPayload, adapter = AssetModuleDeliveryResponse.ADAPTER - ).wrapper?.deliveryInfo -}.onFailure { - Log.d(TAG, "requestAssetModule: ", it) -}.getOrNull() +suspend fun HttpClient.initAssertModuleData( + context: Context, + packageName: String, + accountManager: AccountManager, + requestedAssetModuleNames: List, + playCoreVersionCode: Int, +): ModuleData { + Log.d(TAG, "initAssertModuleData: requestedAssetModuleNames: $requestedAssetModuleNames") + val accounts = accountManager.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + var oauthToken: String? = null + if (accounts.isEmpty()) { + return ModuleData(errorCode = ERROR_CODE_FAIL) + } else for (account: Account in accounts) { + oauthToken = accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) + if (oauthToken != null) { + break + } + } + if (oauthToken == null) { + return ModuleData(errorCode = ERROR_CODE_FAIL) + } + val requestPayload = AssetModuleDeliveryRequest.Builder().callerInfo(CallerInfo(getAppVersionCode(context, packageName)?.toInt())).packageName(packageName) + .playCoreVersion(playCoreVersionCode).pageSource(listOf(PageSource.UNKNOWN_SEARCH_TRAFFIC_SOURCE, PageSource.BOOKS_HOME_PAGE)) + .callerState(listOf(CallerState.CALLER_APP_REQUEST, CallerState.CALLER_APP_DEBUGGABLE)).moduleInfo(ArrayList().apply { + requestedAssetModuleNames.forEach { add(AssetModuleInfo.Builder().name(it).build()) } + }).build() + val moduleDeliveryInfo = runCatching { + post( + url = ASSET_MODULE_DELIVERY_URL, + headers = getLicenseRequestHeaders(oauthToken, 1), + payload = requestPayload, + adapter = AssetModuleDeliveryResponse.ADAPTER + ).wrapper?.deliveryInfo + }.onFailure { + Log.d(TAG, "initAssertModuleData: ", it) + }.getOrNull() + Log.d(TAG, "initAssertModuleData: moduleDeliveryInfo-> $moduleDeliveryInfo") + return initModuleDownloadInfo(context, packageName, moduleDeliveryInfo) +} -suspend fun HttpClient.downloadFile(context: Context, moduleName: String, moduleData: ModuleData, bundle: Bundle) { +suspend fun downloadFile(context: Context, moduleName: String, moduleData: ModuleData, bundle: Bundle) { val resourcePackageName: String? = bundle.getString(KEY_RESOURCE_PACKAGE_NAME) val chunkName: String? = bundle.getString(KEY_CHUNK_NAME) val resourceLink: String? = bundle.getString(KEY_RESOURCE_LINK) @@ -112,27 +149,69 @@ suspend fun HttpClient.downloadFile(context: Context, moduleName: String, module if (destination.exists()) { destination.delete() } - val path = runCatching { download(resourceLink, destination, TAG_REQUEST) }.onFailure { Log.w(TAG, "downloadFile: ", it) }.getOrNull() + val path = runCatching { download(context, resourceLink, destination, moduleName, moduleData) }.onFailure { Log.w(TAG, "downloadFile: ", it) }.getOrNull() if (path != null) { val file = File(path) if (file.exists() && file.length() == byteLength) { - moduleData.updateDownloadStatus(moduleName, STATUS_TRANSFERRING) - moduleData.incrementPackBytesDownloaded(moduleName, byteLength) - moduleData.incrementBytesDownloaded(moduleName) sendBroadcastForExistingFile(context, moduleData, moduleName, bundle, destination) } } } -fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo): ModuleData { +suspend fun download( + context: Context, + url: String, + destinationFile: File, + moduleName: String, + moduleData: ModuleData, +): String = withContext(Dispatchers.IO) { + val uri = Uri.parse(url).toString() + val connection = URL(uri).openConnection() as HttpURLConnection + var bytebit:Long = 0 + try { + connection.requestMethod = "GET" + connection.connectTimeout = 10000 + connection.readTimeout = 10000 + connection.connect() + if (connection.responseCode != HttpURLConnection.HTTP_OK) { + throw IOException("Failed to download file: HTTP response code ${connection.responseCode}") + } + destinationFile.parentFile?.mkdirs() + connection.inputStream.use { input -> + FileOutputStream(destinationFile).use { output -> + val buffer = ByteArray(4096) + var bytesRead: Int + while (input.read(buffer).also { bytesRead = it } != -1) { + output.write(buffer, 0, bytesRead) + moduleData.incrementPackBytesDownloaded(context, moduleName, bytesRead.toLong()) + bytebit += bytesRead + if (bytebit >= 1048576){ + sendBroadcastForExistingFile(context, moduleData, moduleName, null, null) + bytebit = 0 + } + } + } + } + destinationFile.absolutePath + } catch (e: Exception) { + throw IOException("Download failed: ${e.message}", e) + } finally { + connection.disconnect() + } +} + +fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: ModuleDeliveryInfo?): ModuleData { + if (deliveryInfo == null || deliveryInfo.status != null) { + return ModuleData(errorCode = ERROR_CODE_FAIL) + } val packNames: ArrayList = arrayListOf() var moduleDownloadByteLength = 0L - var appVersionCode = 0 + var appVersionCode = 0L val sessionIds = arrayMapOf() val packDataList = arrayMapOf() for (deliveryIndex in deliveryInfo.res.indices) { val resource: ModuleResource = deliveryInfo.res[deliveryIndex] - appVersionCode = resource.versionCode?.toInt() ?: 0 + appVersionCode = resource.versionCode ?: 0 val resourceList: List = resource.packResource val resourcePackageName: String = resource.packName ?: continue var packDownloadByteLength = 0L @@ -175,8 +254,8 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: packVersion = appVersionCode, packBaseVersion = 0, sessionId = STATUS_NOT_INSTALLED, - errorCode = STATUS_SUCCESS, - status = STATUS_NOT_INSTALLED, + errorCode = ERROR_CODE_SUCCESS, + status = STATUS_INITIAL_STATE, bytesDownloaded = 0, totalBytesToDownload = packDownloadByteLength, bundleList = bundlePackageName, @@ -188,10 +267,10 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: } return ModuleData( packageName = packageName, - errorCode = 0, + errorCode = ERROR_CODE_SUCCESS, sessionIds = sessionIds, bytesDownloaded = 0, - status = STATUS_NOT_INSTALLED, + status = STATUS_INITIAL_STATE, packNames = packNames, appVersionCode = appVersionCode, totalBytesToDownload = moduleDownloadByteLength @@ -200,20 +279,20 @@ fun initModuleDownloadInfo(context: Context, packageName: String, deliveryInfo: } } -fun buildDownloadBundle(packName: String, moduleData: ModuleData, isPack: Boolean = false, packNames: ArrayList? = null) = Bundle().apply { +fun buildDownloadBundle(packName: String, moduleData: ModuleData, isPack: Boolean = false, packNames: List? = null) = Bundle().apply { val packData = moduleData.getPackData(packName) packData?.run { putInt(combineModule(KEY_SESSION_ID, packName), sessionId) putInt(combineModule(KEY_STATUS, packName), status) putInt(combineModule(KEY_ERROR_CODE, packName), errorCode) - putInt(combineModule(KEY_PACK_VERSION, packName), packVersion) - putInt(combineModule(KEY_PACK_BASE_VERSION, packName), packBaseVersion) + putLong(combineModule(KEY_PACK_VERSION, packName), packVersion) + putLong(combineModule(KEY_PACK_BASE_VERSION, packName), packBaseVersion) putLong(combineModule(KEY_BYTES_DOWNLOADED, packName), bytesDownloaded) putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, packName), totalBytesToDownload) - putStringArrayList(KEY_PACK_NAMES, packNames ?: if (isPack) arrayListOf(packName) else moduleData.packNames) + putStringArrayList(KEY_PACK_NAMES, packNames?.let { ArrayList(it) } ?: if (isPack) arrayListOf(packName) else moduleData.packNames) putInt(KEY_STATUS, moduleData.status) - putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode) + putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode.toInt()) putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, if (isPack) totalBytesToDownload else moduleData.totalBytesToDownload) putInt(KEY_ERROR_CODE, if (isPack) errorCode else moduleData.errorCode) putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(packName) ?: sessionId) @@ -225,19 +304,19 @@ fun sendBroadcastForExistingFile(context: Context, moduleData: ModuleData, modul val packData = moduleData.getPackData(moduleName) ?: return try { val downloadBundle = Bundle() - downloadBundle.putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode) - downloadBundle.putInt(KEY_ERROR_CODE, STATUS_SUCCESS) + downloadBundle.putInt(KEY_APP_VERSION_CODE, moduleData.appVersionCode.toInt()) + downloadBundle.putInt(KEY_ERROR_CODE, ERROR_CODE_SUCCESS) downloadBundle.putInt(KEY_SESSION_ID, moduleData.sessionIds?.get(moduleName) ?: moduleData.status) downloadBundle.putInt(KEY_STATUS, moduleData.status) downloadBundle.putStringArrayList(KEY_PACK_NAMES, arrayListOf(moduleName)) downloadBundle.putLong(KEY_BYTES_DOWNLOADED, packData.bytesDownloaded) downloadBundle.putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, packData.totalBytesToDownload) downloadBundle.putLong(combineModule(KEY_TOTAL_BYTES_TO_DOWNLOAD, moduleName), packData.totalBytesToDownload) - downloadBundle.putInt(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) + downloadBundle.putLong(combineModule(KEY_PACK_VERSION, moduleName), packData.packVersion) downloadBundle.putInt(combineModule(KEY_STATUS, moduleName), packData.status) - downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), STATUS_SUCCESS) + downloadBundle.putInt(combineModule(KEY_ERROR_CODE, moduleName), ERROR_CODE_SUCCESS) downloadBundle.putLong(combineModule(KEY_BYTES_DOWNLOADED, moduleName), packData.bytesDownloaded) - downloadBundle.putInt(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) + downloadBundle.putLong(combineModule(KEY_PACK_BASE_VERSION, moduleName), packData.packBaseVersion) val resultList = arraySetOf() packData.bundleList?.forEach { val result = Bundle()