From 36651dbdcbea0b03614d592d9538d7ae5b1bc786 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 27 Aug 2024 19:56:38 +0800 Subject: [PATCH] Formatting Code --- vending-app/src/main/AndroidManifest.xml | 6 +- .../microg/vending/billing/core/HttpClient.kt | 35 +- .../SplitInstallService.kt | 96 +++- .../SplitInstallServiceImpl.kt | 446 ------------------ .../finsky/splitinstallservice/extensions.kt | 346 ++++++++++++++ .../phonesky/header/PhoneskyHeaderValue.kt | 107 ----- vending-app/src/main/proto/SplitInstall.proto | 97 ++-- 7 files changed, 517 insertions(+), 616 deletions(-) delete mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallServiceImpl.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/extensions.kt delete mode 100644 vending-app/src/main/kotlin/com/google/android/phonesky/header/PhoneskyHeaderValue.kt diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 35bb3cb6b5..f211be33f8 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -183,9 +183,9 @@ - - + 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 9835e0e8db..d926c95849 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 @@ -10,6 +10,9 @@ 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 @@ -17,7 +20,37 @@ import kotlin.coroutines.suspendCoroutine private const val POST_TIMEOUT = 8000 class HttpClient(context: Context) { - private val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + + 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, diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt index 801f2d254f..87a168211c 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt @@ -5,25 +5,105 @@ package com.google.android.finsky.splitinstallservice +import android.content.Context import android.content.Intent +import android.os.Bundle import android.os.IBinder +import android.util.Log +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.play.core.splitinstall.protocol.ISplitInstallService +import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback +import kotlinx.coroutines.launch import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.core.HttpClient + +private const val TAG = "SplitInstallServiceImpl" class SplitInstallService : LifecycleService() { - private var mService: ISplitInstallService? = null - override fun onCreate() { - super.onCreate() - ProfileManager.ensureInitialized(this) - } + private lateinit var httpClient: HttpClient override fun onBind(intent: Intent): IBinder? { super.onBind(intent) - if (mService == null) { - mService = SplitInstallServiceImpl(this.applicationContext) + Log.d(TAG, "onBind: ") + ProfileManager.ensureInitialized(this) + httpClient = HttpClient(this) + return SplitInstallServiceImpl(this.applicationContext, httpClient, lifecycle).asBinder() + } + + override fun onUnbind(intent: Intent?): Boolean { + Log.d(TAG, "onUnbind: ") + httpClient.requestQueue.cancelAll(SPLIT_INSTALL_REQUEST_TAG) + return super.onUnbind(intent) + } +} + +class SplitInstallServiceImpl(private val context: Context, private val httpClient: HttpClient, override val lifecycle: Lifecycle) : ISplitInstallService.Stub(), LifecycleOwner { + + override fun startInstall(pkg: String, splits: List, bundle0: Bundle, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method Called by package: $pkg") + lifecycleScope.launch { + trySplitInstall(context, httpClient, pkg, splits) + Log.d(TAG, "onStartInstall SUCCESS") + callback.onStartInstall(CommonStatusCodes.SUCCESS, Bundle()) + } + } + + override fun completeInstalls(pkg: String, sessionId: Int, bundle0: Bundle, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (completeInstalls) called but not implement by package -> $pkg") + } + + override fun cancelInstall(pkg: String, sessionId: Int, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (cancelInstall) called but not implement by package -> $pkg") + } + + override fun getSessionState(pkg: String, sessionId: Int, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (getSessionState) called but not implement by package -> $pkg") + } + + override fun getSessionStates(pkg: String, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (getSessionStates) called but not implement by package -> $pkg") + callback.onGetSessionStates(ArrayList(1)) + } + + override fun splitRemoval(pkg: String, splits: List, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (splitRemoval) called but not implement by package -> $pkg") + } + + override fun splitDeferred(pkg: String, splits: List, bundle0: Bundle, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (splitDeferred) called but not implement by package -> $pkg") + callback.onDeferredInstall(Bundle()) + } + + override fun getSessionState2(pkg: String, sessionId: Int, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (getSessionState2) called but not implement by package -> $pkg") + } + + override fun getSessionStates2(pkg: String, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (getSessionStates2) called but not implement by package -> $pkg") + } + + override fun getSplitsAppUpdate(pkg: String, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (getSplitsAppUpdate) called but not implement by package -> $pkg") + } + + override fun completeInstallAppUpdate(pkg: String, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (completeInstallAppUpdate) called but not implement by package -> $pkg") + } + + override fun languageSplitInstall(pkg: String, splits: List, bundle0: Bundle, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method Called by package: $pkg") + lifecycleScope.launch { + trySplitInstall(context, httpClient, pkg, splits) } - return mService as IBinder? } + + override fun languageSplitUninstall(pkg: String, splits: List, callback: ISplitInstallServiceCallback) { + Log.d(TAG, "Method (languageSplitUninstall) called but not implement by package -> $pkg") + } + } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallServiceImpl.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallServiceImpl.kt deleted file mode 100644 index cae695ca79..0000000000 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallServiceImpl.kt +++ /dev/null @@ -1,446 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ -package com.google.android.finsky.splitinstallservice - -import android.accounts.AccountManager -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.Session -import android.os.Build -import android.os.Bundle -import android.os.RemoteException -import android.text.TextUtils -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.R -import com.google.android.phonesky.header.GoogleApiRequest -import com.google.android.play.core.splitinstall.protocol.ISplitInstallService -import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback -import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE -import java.io.BufferedInputStream -import java.io.BufferedOutputStream -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URL -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue -import kotlin.concurrent.thread - -class SplitInstallServiceImpl(private val context: Context) : ISplitInstallService.Stub(){ - - private val tempFilePath = File(context.filesDir,"phonesky-download-service") - - override fun startInstall( - pkg: String, - splits: List, - bundle0: Bundle, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Start install for package: $pkg") - trySplitInstall(pkg, splits, false) - taskQueue.put(Runnable { - try{ - callback.onStartInstall(1, Bundle()) - }catch (ignored: RemoteException){ - } - }) - taskQueue.take().run() - } - - override fun completeInstalls( - pkg: String, - sessionId: Int, - bundle0: Bundle, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Complete installs not implemented") - } - - override fun cancelInstall( - pkg: String, - sessionId: Int, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Cancel install not implemented") - } - - override fun getSessionState( - pkg: String, - sessionId: Int, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "getSessionState not implemented") - } - - override fun getSessionStates(pkg: String, callback: ISplitInstallServiceCallback) { - Log.i(TAG, "getSessionStates for package: $pkg") - callback.onGetSessionStates(ArrayList(1)) - } - - override fun splitRemoval( - pkg: String, - splits: List, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Split removal not implemented") - } - - override fun splitDeferred( - pkg: String, - splits: List, - bundle0: Bundle, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Split deferred not implemented") - callback.onDeferredInstall(Bundle()) - } - - override fun getSessionState2( - pkg: String, - sessionId: Int, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "getSessionState2 not implemented") - } - - override fun getSessionStates2(pkg: String, callback: ISplitInstallServiceCallback) { - Log.i(TAG, "getSessionStates2 not implemented") - } - - override fun getSplitsAppUpdate(pkg: String, callback: ISplitInstallServiceCallback) { - Log.i(TAG, "Get splits for app update not implemented") - } - - override fun completeInstallAppUpdate(pkg: String, callback: ISplitInstallServiceCallback) { - Log.i(TAG, "Complete install for app update not implemented") - } - - override fun languageSplitInstall( - pkg: String, - splits: List, - bundle0: Bundle, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Language split installation requested for $pkg") - trySplitInstall(pkg, splits, true) - taskQueue.take().run() - } - - override fun languageSplitUninstall( - pkg: String, - splits: List, - callback: ISplitInstallServiceCallback - ) { - Log.i(TAG, "Language split uninstallation requested but app not found, package: %s$pkg") - } - - private fun trySplitInstall(pkg: String, splits: List, isLanguageSplit: Boolean) { - Log.d(TAG, "trySplitInstall: $splits") - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - notificationManager.createNotificationChannel( - NotificationChannel( - "splitInstall", - "Split Install", - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - } - val builder = NotificationCompat.Builder(context, "splitInstall") - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentTitle(context.getString(R.string.split_install, context.getString(R.string.app_name))) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setDefaults(NotificationCompat.DEFAULT_ALL) - notificationManager.notify(NOTIFY_ID, builder.build()) - if (isLanguageSplit) { - requestSplitsPackage( - pkg, splits.map { bundle: Bundle -> bundle.getString("language") }.toTypedArray(), - arrayOfNulls(0) - ) - } else { - requestSplitsPackage( - pkg, - arrayOfNulls(0),splits.map { bundle: Bundle -> bundle.getString("module_name") }.toTypedArray()) - } - } - - private fun requestSplitsPackage( - packageName: String, - langName: Array, - splitName: Array - ): Boolean { - Log.d(TAG,"requestSplitsPackage packageName: " + packageName + " langName: " + langName.contentToString() + " splitName: " + splitName.contentToString()) - if(langName.isEmpty() && splitName.isEmpty()){ - return false - } - - val packageManager = context.packageManager - val versionCode = PackageInfoCompat.getLongVersionCode(packageManager.getPackageInfo(packageName, 0)) - val downloadUrls = getDownloadUrls(packageName, langName, splitName, versionCode) - Log.d(TAG, "requestSplitsPackage download url size : " + downloadUrls.size) - if (downloadUrls.isEmpty()){ - Log.w(TAG, "requestSplitsPackage download url is empty") - return false - } - try { - if(!tempFilePath.exists()){ - tempFilePath.mkdir() - } - val language:String? = if (langName.isNotEmpty()) { - langName[0] - } else { - null - } - - taskQueue.put(Runnable { - installSplitPackage(downloadUrls, packageName, language) - }) - - - return true - } catch (e: Exception) { - Log.e("SplitInstallServiceImpl", "Error downloading split", e) - return false - } - } - - private fun downloadSplitPackage(downloadUrls: ArrayList>) : Boolean{ - Log.d(TAG, "downloadSplitPackage downloadUrl:$downloadUrls") - var stat = true - for(downloadUrl in downloadUrls){ - val url = URL(downloadUrl[1]) - val connection = url.openConnection() as HttpURLConnection - connection.readTimeout = 30000 - connection.connectTimeout = 30000 - connection.requestMethod = "GET" - if (connection.responseCode == HttpURLConnection.HTTP_OK) { - BufferedInputStream(connection.inputStream).use { inputstream -> - BufferedOutputStream(FileOutputStream(File(tempFilePath.toString(),downloadUrl[0]))).use { outputstream -> - inputstream.copyTo(outputstream) - } - } - }else{ - stat = false - } - Log.d(TAG, "downloadSplitPackage code: " + connection.responseCode) - } - return stat - } - - private fun installSplitPackage( - downloadUrl: ArrayList>, - packageName: String, - language: String? - ) { - try { - Log.d(TAG, "installSplitPackage downloadUrl:$downloadUrl") - if (downloadSplitPackage(downloadUrl)) { - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.cancel(NOTIFY_ID) - val packageInstaller: PackageInstaller - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - packageInstaller = context.packageManager.packageInstaller - val params = PackageInstaller.SessionParams( - PackageInstaller.SessionParams.MODE_INHERIT_EXISTING - ) - params.setAppPackageName(packageName) - params.setAppLabel(packageName + "Subcontracting") - params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) - try { - @SuppressLint("PrivateApi") val method = - PackageInstaller.SessionParams::class.java.getDeclaredMethod( - "setDontKillApp", - Boolean::class.javaPrimitiveType - ) - method.invoke(params, true) - } catch (e: Exception) { - Log.w(TAG, "Error setting dontKillApp", e) - } - - val sessionId: Int - var session : Session? = null - var totalDownloaded = 0L - try { - sessionId = packageInstaller.createSession(params) - session = packageInstaller.openSession(sessionId) - - try { - downloadUrl.forEach { item -> - val pkgPath = File(tempFilePath.toString(),item[0]) - session.openWrite(item[0], 0, -1).use { outputStream -> - FileInputStream(pkgPath).use { inputStream -> - inputStream.copyTo(outputStream) - } - session.fsync(outputStream) - } - - totalDownloaded += pkgPath.length() - pkgPath.delete() - } - } catch (e: Exception) { - Log.e(TAG, "Error installing split", e) - } - - val intent = Intent(context, InstallResultReceiver::class.java) - intent.putExtra("pkg", packageName) - intent.putExtra("language", language) - intent.putExtra("bytes_downloaded", totalDownloaded) - val pendingIntent = PendingIntent.getBroadcast(context,sessionId, intent, 0) - session.commit(pendingIntent.intentSender) - Log.d(TAG, "installSplitPackage commit") - } catch (e: IOException) { - Log.w(TAG, "Error installing split", e) - } finally { - session?.close() - } - } - } else { - taskQueue.clear(); - Log.w(TAG, "installSplitPackage download failed") - } - } catch (e: Exception) { - Log.w(TAG, "downloadSplitPackage: ", e) - } - } - - private fun getDownloadUrls( - packageName: String, - langName: Array, - splitName: Array, - versionCode: Long - ): ArrayList> { - Log.d(TAG, "getDownloadUrls: ") - val downloadUrls = ArrayList>() - try { - val requestUrl = StringBuilder( - "https://play-fe.googleapis.com/fdfe/delivery?doc=" + packageName + "&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=" - ) - for (language in langName) { - requestUrl.append("&mn=config.").append(language) - } - for (split in splitName) { - requestUrl.append("&mn=").append(split) - } - val accounts = AccountManager.get(this.context).getAccountsByType(DEFAULT_ACCOUNT_TYPE) - if (accounts.isEmpty()) { - Log.w(TAG, "getDownloadUrls account is null") - return downloadUrls - } - val googleApiRequest = - GoogleApiRequest( - requestUrl.toString(), "GET", accounts[0], context, - langName.filterNotNull() - ) - val response = googleApiRequest.sendRequest(null) - val pkgs = response?.fdfeApiResponseValue?.splitReqResult?.pkgList?.pkgDownlaodInfo - if (pkgs != null) { - for (item in pkgs) { - for (lang in langName) { - if (TextUtils.equals("config.$lang", item.splitPkgName) || "config.$lang".startsWith(item.splitPkgName!!)) { - downloadUrls.add(arrayOf(lang!!, item.downloadUrl1!!)) - } - } - Log.d(TAG, "requestSplitsPackage: $splitName") - for (split in splitName) { - if (split != null && TextUtils.equals(split, item.splitPkgName)) { - downloadUrls.add(arrayOf(split, item.downloadUrl1!!)) - } - } - } - } - } catch (e: Exception) { - Log.w(TAG, "Error getting download url", e) - } - return downloadUrls - } - - class InstallResultReceiver : BroadcastReceiver() { - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - override fun onReceive(context: Context, intent: Intent) { - val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) - Log.d(TAG, "onReceive status: $status") - try { - when (status) { - PackageInstaller.STATUS_SUCCESS -> { - if (taskQueue.isNotEmpty()) { - thread { - taskQueue.take().run() - } - } - if(taskQueue.size <= 1){ - NotificationManagerCompat.from(context).cancel(NOTIFY_ID) - sendCompleteBroad(context, intent) - } - } - - PackageInstaller.STATUS_FAILURE -> { - taskQueue.clear(); - val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - Log.d("InstallResultReceiver", errorMsg ?: "") - } - - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val extraIntent = intent.extras!![Intent.EXTRA_INTENT] as Intent? - extraIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ContextCompat.startActivity(context, extraIntent, null) - } - - else -> { - taskQueue.clear() - NotificationManagerCompat.from(context).cancel(NOTIFY_ID) - val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - Log.d("InstallResultReceiver", errorMsg ?: "") - Log.w(TAG, "onReceive: install fail") - } - } - } catch (e: Exception) { - taskQueue.clear() - NotificationManagerCompat.from(context).cancel(NOTIFY_ID) - Log.w(TAG, "Error handling install result", e) - } - } - - private fun sendCompleteBroad(context: Context, originalIntent: Intent) { - Log.d(TAG, "sendCompleteBroadcast: $originalIntent") - val extra = Bundle().apply { - putInt("status", 5) - putLong("total_bytes_to_download", originalIntent.getLongExtra("bytes_downloaded", 0)) - putString("languages", originalIntent.getStringExtra("language")) - putInt("error_code", 0) - putInt("session_id", 0) - putLong("bytes_downloaded", originalIntent.getLongExtra("bytes_downloaded", 0)) - } - val broadcastIntent = Intent("com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService").apply { - setPackage(originalIntent.getStringExtra("pkg")) - putExtra("session_state", extra) - addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - } - context.sendBroadcast(broadcastIntent) - } - } - - companion object { - private val taskQueue: BlockingQueue = LinkedBlockingQueue() - val TAG: String = SplitInstallServiceImpl::class.java.simpleName - const val NOTIFY_ID = 111 - } -} - diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/extensions.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/extensions.kt new file mode 100644 index 0000000000..00264f812f --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/extensions.kt @@ -0,0 +1,346 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +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.database.Cursor +import android.os.Build +import android.os.Bundle +import android.util.Base64 +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.R +import com.android.vending.RequestLanguagePackage +import com.android.vending.licensing.AUTH_TOKEN_SCOPE +import com.android.vending.licensing.encodeGzip +import com.android.vending.licensing.getAuthToken +import com.android.vending.licensing.getDefaultLicenseRequestHeaderBuilder +import com.android.vending.licensing.getLicenseRequestHeaders +import com.google.android.finsky.GoogleApiResponse +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import org.microg.gms.settings.SettingsContract +import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE +import org.microg.vending.billing.core.HttpClient +import java.io.File +import java.io.FileInputStream +import java.io.IOException + +const val SPLIT_INSTALL_REQUEST_TAG = "splitInstallRequestTag" +private const val SPLIT_INSTALL_NOTIFY_ID = 111 + +private const val NOTIFY_CHANNEL_ID = "splitInstall" +private const val NOTIFY_CHANNEL_NAME = "Split Install" +private const val KEY_LANGUAGE = "language" +private const val KEY_LANGUAGES = "languages" +private const val KEY_PACKAGE = "pkg" +private const val KEY_MODULE_NAME = "module_name" +private const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" +private const val KEY_TOTAL_BYTES_TO_DOWNLOAD = "total_bytes_to_download" +private const val KEY_STATUS = "status" +private const val KEY_ERROR_CODE = "error_code" +private const val KEY_SESSION_ID = "session_id" +private const val KEY_SESSION_STATE = "session_state" + +private const val ACTION_UPDATE_SERVICE = "com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService" + +private const val FILE_SAVE_PATH = "phonesky-download-service" +private const val TAG = "SplitInstallExtensions" + +private val mutex = Mutex() +private val deferredMap = mutableMapOf>() + +private var lastSplitPackageName: String? = null +private val splitRecord = arrayListOf>() + +private fun Context.splitSaveFile() = File(filesDir, FILE_SAVE_PATH) + +suspend fun trySplitInstall(context: Context, httpClient: HttpClient, pkg: String, splits: List) { + if (lastSplitPackageName != null && lastSplitPackageName != pkg && mutex.isLocked) { + mutex.unlock() + } + mutex.withLock { + Log.d(TAG, "trySplitInstall: pkg: $pkg") + var splitNames:Array ?= null + try { + if (splits.any { it.getString(KEY_LANGUAGE) != null }) { + splitNames = splits.mapNotNull { bundle -> bundle.getString(KEY_LANGUAGE) }.toTypedArray() + Log.d(TAG, "langNames: ${splitNames.contentToString()}") + if (splitNames.isEmpty() || splitRecord.any { splitNames.contentEquals(it) }) { + return@withLock + } + lastSplitPackageName = pkg + requestSplitsPackage(context, httpClient, pkg, splitNames, emptyArray()) + splitRecord.add(splitNames) + } else if (splits.any { it.getString(KEY_MODULE_NAME) != null }) { + splitNames = splits.mapNotNull { bundle -> bundle.getString(KEY_MODULE_NAME) }.toTypedArray() + Log.d(TAG, "moduleNames: ${splitNames.contentToString()}") + if (splitNames.isEmpty() || splitRecord.any { splitNames.contentEquals(it) }) { + return@withLock + } + lastSplitPackageName = pkg + requestSplitsPackage(context, httpClient, pkg, emptyArray(), splitNames) + splitRecord.add(splitNames) + } + } catch (e: Exception) { + Log.w(TAG, "Error downloading split", e) + splitNames?.run { splitRecord.remove(this) } + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + } + return@withLock + } +} + +private fun notify(context: Context) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + notificationManager.createNotificationChannel( + NotificationChannel(NOTIFY_CHANNEL_ID, NOTIFY_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT) + ) + } + NotificationCompat.Builder(context, NOTIFY_CHANNEL_ID).setSmallIcon(android.R.drawable.stat_sys_download) + .setContentTitle(context.getString(R.string.split_install, context.getString(R.string.app_name))).setPriority(NotificationCompat.PRIORITY_DEFAULT).setDefaults(NotificationCompat.DEFAULT_ALL) + .build().also { + notificationManager.notify(SPLIT_INSTALL_NOTIFY_ID, it) + } +} + +private suspend fun requestSplitsPackage(context: Context, httpClient: HttpClient, packageName: String, langName: Array, splitName: Array) { + Log.d(TAG, "requestSplitsPackage packageName: $packageName langName: ${langName.contentToString()} splitName: ${splitName.contentToString()}") + notify(context) + val downloadUrls = getDownloadUrls(context, httpClient, packageName, langName, splitName) + Log.d(TAG, "requestSplitsPackage download url size : " + downloadUrls.size) + if (downloadUrls.isEmpty()) { + throw RuntimeException("requestSplitsPackage download url is empty") + } + if (!context.splitSaveFile().exists()) { + context.splitSaveFile().mkdir() + } + val intent = installSplitPackage(context, httpClient, downloadUrls, packageName, langName.firstOrNull()) + sendCompleteBroad(context, intent) +} + +private suspend fun getDownloadUrls(context: Context, httpClient: HttpClient, packageName: String, langName: Array, splitName: Array): ArrayList> { + Log.d(TAG, "getDownloadUrls: start -> langName:${langName.contentToString()} splitName:${splitName.contentToString()}") + val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(packageName, 0)) + val requestUrl = StringBuilder( + "https://play-fe.googleapis.com/fdfe/delivery?doc=$packageName&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=" + ) + for (language in langName) { + requestUrl.append("&mn=config.").append(language) + } + for (split in splitName) { + requestUrl.append("&mn=").append(split) + } + val accounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE) + var oauthToken: String? = null + if (accounts.isEmpty()) { + throw RuntimeException("No Google account found") + } else for (account: Account in accounts) { + oauthToken = try { + AccountManager.get(context).getAuthToken(account, AUTH_TOKEN_SCOPE, false).getString(AccountManager.KEY_AUTHTOKEN) + } catch (e: AuthenticatorException) { + Log.w(TAG, "Could not fetch auth token for account $account") + null + } + if (oauthToken != null) { + break + } + } + if (oauthToken == null) { + throw RuntimeException("account oauthToken is null") + } + Log.d(TAG, "getDownloadUrls: requestDownloadUrl start") + val response = httpClient.requestDownloadUrl(context, requestUrl.toString(), oauthToken, langName.toList()) + Log.d(TAG, "getDownloadUrls: requestDownloadUrl end response -> $response") + val splitPkgInfoList = response?.response?.splitReqResult?.pkgList?.pkgDownLoadInfo ?: throw RuntimeException("splitPkgInfoList is null") + val downloadUrls = ArrayList>() + splitPkgInfoList.filter { !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() }.forEach { info -> + langName.filter { "config.$it".contains(info.splitPkgName!!) }.forEach { downloadUrls.add(arrayOf(it, info.downloadUrl!!)) } + splitName.filter { it.contains(info.splitPkgName!!) }.forEach { downloadUrls.add(arrayOf(it, info.downloadUrl!!)) } + } + return downloadUrls +} + +private suspend fun HttpClient.requestDownloadUrl(context: Context, requestUrl: String, auth: String, requestLanguagePackage: List) = runCatching { + val androidId = SettingsContract.getSettings( + context, SettingsContract.CheckIn.getContentUri(context), arrayOf(SettingsContract.CheckIn.ANDROID_ID) + ) { cursor: Cursor -> cursor.getLong(0) } + Log.d(TAG, "requestUrl->$requestUrl") + Log.d(TAG, "auth->$auth") + Log.d(TAG, "androidId->$androidId") + Log.d(TAG, "requestLanguagePackage->$requestLanguagePackage") + get(url = requestUrl, headers = getLicenseRequestHeaders(auth, 1).toMutableMap().apply { + val xPsRh = String( + Base64.encode( + getDefaultLicenseRequestHeaderBuilder(1).languages(RequestLanguagePackage.Builder().language(requestLanguagePackage).build()).build().encode().encodeGzip(), + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ) + ) + put("X-PS-RH", xPsRh) + }.onEach { + Log.d(TAG, "key:${it.key} value:${it.value}") + }, adapter = GoogleApiResponse.ADAPTER) +}.onFailure { + Log.d(TAG, "requestDownloadUrl: ", it) +}.getOrNull() + +private suspend fun HttpClient.downloadSplitPackage(context: Context, downloadUrls: ArrayList>): Boolean = coroutineScope { + val results = downloadUrls.map { urls -> + Log.d(TAG, "downloadSplitPackage: ${urls.contentToString()}") + async { + runCatching { + download(urls[1], File(context.splitSaveFile().toString(), urls[0]), SPLIT_INSTALL_REQUEST_TAG) + }.onFailure { + Log.w(TAG, "downloadSplitPackage urls:${urls.contentToString()}: ", it) + }.getOrNull() != null + } + }.awaitAll() + return@coroutineScope results.all { it } +} + +private suspend fun installSplitPackage(context: Context, httpClient: HttpClient, downloadUrl: ArrayList>, packageName: String, language: String?): Intent { + Log.d(TAG, "installSplitPackage downloadUrl: ${downloadUrl.firstOrNull()}") + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + throw RuntimeException("installSplitPackage Not supported yet ") + } + val downloadSplitPackage = httpClient.downloadSplitPackage(context, downloadUrl) + if (!downloadSplitPackage) { + Log.w(TAG, "installSplitPackage download failed") + throw RuntimeException("installSplitPackage downloadSplitPackage has error") + } + Log.d(TAG, "installSplitPackage downloaded success") + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.cancel(SPLIT_INSTALL_NOTIFY_ID) + val packageInstaller = context.packageManager.packageInstaller + val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) + params.setAppPackageName(packageName) + params.setAppLabel(packageName + "Subcontracting") + params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) + try { + @SuppressLint("PrivateApi") val method = PackageInstaller.SessionParams::class.java.getDeclaredMethod( + "setDontKillApp", Boolean::class.javaPrimitiveType + ) + method.invoke(params, true) + } catch (e: Exception) { + Log.w(TAG, "Error setting dontKillApp", e) + } + val sessionId: Int + var session: PackageInstaller.Session? = null + var totalDownloaded = 0L + try { + sessionId = packageInstaller.createSession(params) + session = packageInstaller.openSession(sessionId) + downloadUrl.forEach { item -> + val pkgPath = File(context.splitSaveFile().toString(), item[0]) + session.openWrite(item[0], 0, -1).use { outputStream -> + FileInputStream(pkgPath).use { inputStream -> inputStream.copyTo(outputStream) } + session.fsync(outputStream) + } + totalDownloaded += pkgPath.length() + pkgPath.delete() + } + + val deferred = CompletableDeferred() + deferredMap[sessionId] = deferred + val intent = Intent(context, InstallResultReceiver::class.java).apply { + putExtra(KEY_PACKAGE, packageName) + putExtra(KEY_LANGUAGE, language) + putExtra(KEY_BYTES_DOWNLOADED, totalDownloaded) + } + val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, 0) + session.commit(pendingIntent.intentSender) + Log.d(TAG, "installSplitPackage session commit") + return deferred.await() + } catch (e: IOException) { + Log.w(TAG, "Error installing split", e) + throw e + } finally { + session?.close() + } +} + +private fun sendCompleteBroad(context: Context, intent: Intent) { + Log.d(TAG, "sendCompleteBroadcast: intent:$intent") + val extra = Bundle().apply { + putInt(KEY_STATUS, 5) + putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0)) + putString(KEY_LANGUAGES, intent.getStringExtra(KEY_LANGUAGE)) + putInt(KEY_ERROR_CODE, 0) + putInt(KEY_SESSION_ID, 0) + putLong(KEY_BYTES_DOWNLOADED, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0)) + } + val broadcastIntent = Intent(ACTION_UPDATE_SERVICE).apply { + setPackage(intent.getStringExtra(KEY_PACKAGE)) + putExtra(KEY_SESSION_STATE, extra) + addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + } + context.sendBroadcast(broadcastIntent) +} + +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") + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + 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.d(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) + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + if (sessionId != -1) { + deferredMap[sessionId]?.completeExceptionally(e) + } + } + } +} + diff --git a/vending-app/src/main/kotlin/com/google/android/phonesky/header/PhoneskyHeaderValue.kt b/vending-app/src/main/kotlin/com/google/android/phonesky/header/PhoneskyHeaderValue.kt deleted file mode 100644 index b92df2d5ed..0000000000 --- a/vending-app/src/main/kotlin/com/google/android/phonesky/header/PhoneskyHeaderValue.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.google.android.phonesky.header - -import android.accounts.Account -import android.accounts.AccountManager -import android.content.Context -import android.database.Cursor -import android.util.Base64 -import android.util.Log -import com.android.vending.RequestLanguagePackage -import com.android.vending.licensing.AUTH_TOKEN_SCOPE -import com.android.vending.licensing.encodeGzip -import com.android.vending.licensing.getDefaultLicenseRequestHeaderBuilder -import com.android.vending.licensing.getLicenseRequestHeaders -import org.microg.gms.common.Utils -import org.microg.gms.settings.SettingsContract -import java.io.DataOutputStream -import java.io.OutputStream -import java.net.HttpURLConnection -import java.net.URL -import java.util.zip.GZIPOutputStream - - -private const val TAG = "GoogleApiRequest" -class GoogleApiRequest( - private var url: String, - private var method: String, - private val account: Account, - private var context: Context, - private val requestLanguagePackage: List -) { - private var content: ByteArray? = null - private var timeout: Int = 3000 - private var gzip: Boolean = false - - private fun getHeaders(): Map { - - val auth = AccountManager.get(context).getAuthToken( - account, AUTH_TOKEN_SCOPE, null, false, null, null - ).result.getString(AccountManager.KEY_AUTHTOKEN) ?: "" - - if (auth.isEmpty()) { - Log.w(TAG, "authToken is Empty!") - } - - val androidId = SettingsContract.getSettings( - context, - SettingsContract.CheckIn.getContentUri(context), - arrayOf(SettingsContract.CheckIn.ANDROID_ID) - ) { cursor: Cursor -> cursor.getLong(0) } - - val xPsRh = String(Base64.encode(getDefaultLicenseRequestHeaderBuilder(androidId) - .languages(RequestLanguagePackage.Builder().language(requestLanguagePackage).build()) - .build() - .encode() - .encodeGzip(),Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)) - - val headerMap = getLicenseRequestHeaders(auth, androidId).toMutableMap() - headerMap["X-PS-RH"] = xPsRh - return headerMap - } - - fun sendRequest(externalHeader: Map?): GoogleApiResponse? { - val requestUrl = URL(this.url) - val httpURLConnection = requestUrl.openConnection() as HttpURLConnection - httpURLConnection.instanceFollowRedirects = HttpURLConnection.getFollowRedirects() - httpURLConnection.connectTimeout = timeout - httpURLConnection.readTimeout = timeout - httpURLConnection.useCaches = false - httpURLConnection.doInput = true - - val headers: MutableMap = HashMap( - this.getHeaders() - ) - if (externalHeader != null) headers.putAll(externalHeader) - for (key in headers.keys) { - httpURLConnection.setRequestProperty(key, headers[key]) - } - httpURLConnection.requestMethod = method - if (this.method == "POST") { - val content = this.content - if (content != null) { - httpURLConnection.doInput = true - if (!httpURLConnection.requestProperties.containsKey("Content-Type")) { - httpURLConnection.setRequestProperty( - "Content-Type", - "application/x-protobuf" - ) - } - val dataOutputStream: OutputStream = if (this.gzip) { - GZIPOutputStream(DataOutputStream(httpURLConnection.outputStream)) - } else { - DataOutputStream(httpURLConnection.outputStream) - } - - dataOutputStream.write(content) - dataOutputStream.close() - } - } - val responseCode = httpURLConnection.responseCode - if (responseCode == HttpURLConnection.HTTP_OK) { - val data = Utils.readStreamToEnd(httpURLConnection.inputStream) - return GoogleApiResponse.ADAPTER.decode(data) - } - - return null - } -} \ No newline at end of file diff --git a/vending-app/src/main/proto/SplitInstall.proto b/vending-app/src/main/proto/SplitInstall.proto index 0308b44e43..38c7a50043 100644 --- a/vending-app/src/main/proto/SplitInstall.proto +++ b/vending-app/src/main/proto/SplitInstall.proto @@ -1,88 +1,83 @@ -option java_package = "com.google.android.phonesky.header"; +option java_package = "com.google.android.finsky"; option java_multiple_files = true; message GoogleApiResponse { - optional FdfeApiResponse fdfeApiResponseValue = 1; - optional UnknowTypebbfe g= 5; - optional bytes unknowFieldBytes= 9; + optional ApiResponse response = 1; + optional UnknownType type= 5; + optional bytes unknownFieldBytes= 9; } -message UnknowTypebbfe { +message UnknownType { optional int64 id=1; } -message FdfeApiResponse { +message ApiResponse { optional TocResponse tocApi = 6; optional SplitResponse splitReqResult = 21; - optional SyncApiResp syncResult = 183; +// optional SyncApiResp syncResult = 183; } message TocResponse { -// optional bool o=11; - optional string tocTokenValue=22; //t + optional string tocTokenValue = 22; } message SplitResponse { - optional int32 b = 1; //unknow enum + optional int32 unknownInt32 = 1; optional PkgFetchInfo pkgList = 2; } message PkgFetchInfo { - repeated SplitPkgInfo pkgDownlaodInfo = 15; + repeated SplitPkgInfo pkgDownLoadInfo = 15; } message SplitPkgInfo { optional string splitPkgName = 1; optional int64 size = 2; optional string checkSum = 4; - optional string downloadUrl1 = 5; + optional string downloadUrl = 5; optional DownloadInfo slaveDownloadInfo = 8; - optional string mabyChecksum = 9; - optional string unknowPkgInfoF = 15; + optional string checksum = 9; + optional string unknownPkgInfoString = 15; optional DownloadInfo otherDownloadInfo = 16; } message DownloadInfo { - optional int32 id = 1; //unknow enum + optional int32 id = 1; optional int64 size = 2; optional string url = 3; } - - -//-----------------response for fdfe/sync -message SyncApiResp { - repeated SyncApiRespEmptyA unknowFieldA=1; - optional SyncToken syncTokenValue=2; -// repeated string c=3; -} - -message SyncToken { - optional string mvalue = 1; -} - - -message SyncApiRespEmptyA { - oneof b { - UnknowTypeaynt unknowEmptyField = 2; -// aynp oneofField1 = 3; - } - optional int64 id=1; -} - -message UnknowTypeaynt { - optional UnknowEmptyAynx a=1; - optional int32 id=2; //unknow enum -} - -message UnknowEmptyAynx { - oneof b { - UnknowTypeawwm oneofField25 = 26; - } -} - -message UnknowTypeawwm { - optional int32 id=1; -} +//message SyncApiResp { +// repeated SyncRespContent content = 1; +// optional SyncToken syncTokenValue = 2; +//// repeated string c=3; +//} +// +//message SyncToken { +// optional string mvalue = 1; +//} +// +// +//message SyncRespContent { +// oneof b { +// UnknowTypeaynt unknowEmptyField = 2; +// } +// optional int64 token = 1; +//} +// +//message UnknownType { +// optional UnknowEmptyAynx a=1; +// optional int32 id=2; //unknow enum +//} +// +//message UnknowEmptyAynx { +// oneof b { +// UnknowTypeawwm oneofField25 = 26; +// } +//} +// +//message UnknowTypeawwm { +// optional int32 id=1; +//}