From 34ccbe2c00a1231aa7ec4dcd967843eebb2a5da8 Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Mon, 19 Aug 2024 19:22:28 +0800 Subject: [PATCH 01/11] SplitInstallService Additions --- build.gradle | 2 +- .../splitinstallservice/ExampleUnitTest.java | 17 + vending-app/build.gradle | 1 + vending-app/src/main/AndroidManifest.xml | 18 + .../protocol/ISplitInstallService.aidl | 22 + .../ISplitInstallServiceCallback.aidl | 19 + .../org/microg/vending/billing/Constants.kt | 4 +- .../org/microg/vending/billing/GServices.kt | 1 + .../android/vending/ExperimentAndConfigs.kt | 839 ++++++++++++++++++ .../com/android/vending/MainApplication.kt | 93 ++ .../com/android/vending/PhenotypeDatabase.kt | 58 ++ .../SplitInstallService.kt | 28 + .../SplitInstallServiceImpl.kt | 451 ++++++++++ .../phonesky/header/PayloadsProtoStore.kt | 777 ++++++++++++++++ .../phonesky/header/PhoneskyHeaderValue.kt | 406 +++++++++ .../main/proto/ExperimentsAndConfigs.proto | 167 ++++ .../main/proto/PhoneskyHeaderValueStore.proto | 433 +++++++++ .../src/main/res/values-zh-rCN/strings.xml | 1 + vending-app/src/main/res/values/strings.xml | 1 + 19 files changed, 3336 insertions(+), 2 deletions(-) create mode 100644 play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallService.aidl create mode 100644 vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl create mode 100644 vending-app/src/main/kotlin/com/android/vending/ExperimentAndConfigs.kt create mode 100644 vending-app/src/main/kotlin/com/android/vending/MainApplication.kt create mode 100644 vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt create 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/phonesky/header/PayloadsProtoStore.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/phonesky/header/PhoneskyHeaderValue.kt create mode 100644 vending-app/src/main/proto/ExperimentsAndConfigs.proto create mode 100644 vending-app/src/main/proto/PhoneskyHeaderValueStore.proto diff --git a/build.gradle b/build.gradle index 92339b13f2..5e9d0ecedc 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ buildscript { ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' - ext.wireVersion = '4.8.0' + ext.wireVersion = '4.9.9' ext.androidBuildGradleVersion = '8.2.2' diff --git a/play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java b/play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java new file mode 100644 index 0000000000..274bc2af20 --- /dev/null +++ b/play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.google.android.finsky.splitinstallservice; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/vending-app/build.gradle b/vending-app/build.gradle index c8281eef76..c60f8261be 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -107,6 +107,7 @@ dependencies { implementation 'androidx.activity:activity-compose:1.7.2' implementation("io.coil-kt:coil-compose:2.4.0") implementation("io.coil-kt:coil-svg:2.2.2") + implementation 'org.brotli:dec:0.1.2' implementation "com.google.android.material:material:$materialVersion" implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0" diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index f370344a37..690446c4c6 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + + + @@ -93,6 +99,7 @@ + @@ -148,6 +155,13 @@ + + + + + + @@ -175,5 +189,9 @@ + + + diff --git a/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallService.aidl b/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallService.aidl new file mode 100644 index 0000000000..6a87bffe74 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallService.aidl @@ -0,0 +1,22 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.play.core.splitinstall.protocol; +import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback; + +interface ISplitInstallService { + void startInstall(String pkg,in List splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 1; + void completeInstalls(String pkg, int sessionId,in Bundle bundle, ISplitInstallServiceCallback callback) = 2; + void cancelInstall(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 3; + void getSessionState(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 4; + void getSessionStates(String pkg, ISplitInstallServiceCallback callback) = 5; + void splitRemoval(String pkg,in List splits, ISplitInstallServiceCallback callback) = 6; + void splitDeferred(String pkg,in List splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 7; + void getSessionState2(String pkg, int sessionId, ISplitInstallServiceCallback callback) = 8; + void getSessionStates2(String pkg, ISplitInstallServiceCallback callback) = 9; + void getSplitsAppUpdate(String pkg, ISplitInstallServiceCallback callback) = 10; + void completeInstallAppUpdate(String pkg, ISplitInstallServiceCallback callback) = 11; + void languageSplitInstall(String pkg,in List splits,in Bundle bundle, ISplitInstallServiceCallback callback) = 12; + void languageSplitUninstall(String pkg,in List splits, ISplitInstallServiceCallback callback) =13; +} \ No newline at end of file diff --git a/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl new file mode 100644 index 0000000000..b3952859d1 --- /dev/null +++ b/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl @@ -0,0 +1,19 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.play.core.splitinstall.protocol; + + +interface ISplitInstallServiceCallback { + void a(int v,in Bundle bundle0) = 3; + void b(in Bundle bundle0) = 6; + void c(int v,in Bundle bundle0) = 4; + void d(in Bundle bundle0) = 9; + void e(in Bundle bundle0) = 12; + void f(in Bundle bundle0) = 13; + void g(in Bundle bundle0) = 8; + void h(int v,in Bundle bundle0) = 5; + void i(in List list0) = 7; + void j(int v,in Bundle bundle0) = 2; +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/Constants.kt b/vending-app/src/main/java/org/microg/vending/billing/Constants.kt index 93af4deb03..4659a644a5 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/Constants.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/Constants.kt @@ -14,4 +14,6 @@ const val VENDING_PACKAGE_NAME = "com.android.vending" // TODO: Replace key name const val KEY_IAP_SHEET_UI_PARAM = "key_iap_sheet_ui_param" const val DEFAULT_ACCOUNT_TYPE = "com.google" -const val ADD_PAYMENT_METHOD_URL = "https://play.google.com/store/paymentmethods" \ No newline at end of file +const val ADD_PAYMENT_METHOD_URL = "https://play.google.com/store/paymentmethods" +const val FINSKY_REGULAR = "com.google.android.finsky.regular" +const val FINSKY_STABLE = "com.google.android.finsky.stable" \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/GServices.kt b/vending-app/src/main/java/org/microg/vending/billing/GServices.kt index 60b151324d..9e43d0d506 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/GServices.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/GServices.kt @@ -7,6 +7,7 @@ import android.net.Uri object GServices { private val CONTENT_URI: Uri = Uri.parse("content://com.google.android.gsf.gservices") + fun getString(resolver: ContentResolver, key: String, defaultValue: String?): String? { var result = defaultValue val cursor = resolver.query(CONTENT_URI, null, null, arrayOf(key), null) diff --git a/vending-app/src/main/kotlin/com/android/vending/ExperimentAndConfigs.kt b/vending-app/src/main/kotlin/com/android/vending/ExperimentAndConfigs.kt new file mode 100644 index 0000000000..452f0a3bea --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/ExperimentAndConfigs.kt @@ -0,0 +1,839 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.android.vending + +import android.accounts.Account +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.net.Uri +import android.os.Build +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.annotation.RequiresApi +import com.google.android.finsky.ApplicationTag +import com.google.android.finsky.DeviceData +import com.google.android.finsky.DeviceDataEmptyA +import com.google.android.finsky.ExpDeviceInfo +import com.google.android.finsky.ExpDeviceInfoWrapper +import com.google.android.finsky.ExperimentFlag +import com.google.android.finsky.ExperimentResponseData +import com.google.android.finsky.ExperimentTokenStore +import com.google.android.finsky.ExperimentVersion +import com.google.android.finsky.ExperimentsDataWrapper +import com.google.android.finsky.ExperimentsFlagsProto +import com.google.android.finsky.ExperimentsInfo +import com.google.android.finsky.FlagValueProto +import com.google.android.finsky.UnknowMsg +import com.google.android.finsky.action +import com.google.android.finsky.experimentRequestData +import okio.ByteString +import okio.ByteString.Companion.encodeUtf8 +import org.microg.vending.billing.FINSKY_REGULAR +import org.microg.vending.billing.FINSKY_STABLE +import org.microg.vending.billing.GServices.getString +import org.microg.vending.billing.VENDING_PACKAGE_NAME +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.util.Arrays +import java.util.List +import java.util.Locale +import java.util.Objects +import java.util.TreeSet +import java.util.zip.GZIPOutputStream + +object ExperimentAndConfigs { + val TAG: String = ExperimentAndConfigs::class.java.simpleName + private const val version = 84122130L + private const val baselineCL = 636944598L + + private fun buildBaseGpInfo(pkgName: String, fixed64: Long): ExperimentsInfo.Builder { + val experimentFlag = ExperimentFlag.Builder().flag(fixed64).build() + val experimentInfo = ExperimentVersion.Builder() + .expPkgName(pkgName) + .version(version) + .experimentFlagValue(experimentFlag) + .baselineCL(baselineCL) //cli + .pkgName(VENDING_PACKAGE_NAME).build() + + val msg = UnknowMsg.Builder().field1(1).build() + return ExperimentsInfo.Builder() + .experimentVersionValue(experimentInfo) + .unKnowBytesC(msg.encodeByteString()) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun buildRequestData(context: Context): experimentRequestData { + return buildRequestData(context, "NEW_USER_SYNC", null, null) + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun buildRequestData( + context: Context, + actions: String, + pkgName: String?, + account: Account? + ): experimentRequestData { + @SuppressLint("HardwareIds") val deviceData = createDeviceData(account, context) + val finskyRegularInfo: ExperimentsInfo.Builder + var finskyStableInfo: ExperimentsInfo.Builder? = null + if (actions == "NEW_USER_SYNC_ALL_ACCOUNT") { + finskyRegularInfo = buildBaseGpInfo(FINSKY_REGULAR, -1) + finskyStableInfo = buildBaseGpInfo(FINSKY_STABLE, -1) + } else { + finskyRegularInfo = buildBaseGpInfo(FINSKY_REGULAR, 0) + } + + if (actions.contains("NEW_USER_SYNC")) { + val result = experimentRequestData.Builder() + result.deviceDataValue(deviceData) + .bytesTag(ByteString.of()) + .actionType(action.NEW_USER_SYNC) + .unknowFieldG(128) + .expPkgName(FINSKY_REGULAR) + val experimentsInfoValue = result.experimentsInfo.toMutableList() + experimentsInfoValue.add(finskyRegularInfo.build()) + if (finskyStableInfo != null) experimentsInfoValue.add(finskyStableInfo.build()) + result.experimentsInfo = experimentsInfoValue + return result.build() + } + + if (actions == "NEW_APPLICATION_SYNC") { + try { + PhenotypeDatabase(context).writableDatabase.use { db -> + val applicationTags: MutableList = ArrayList() + db.rawQuery( + "SELECT partitionId, tag FROM ApplicationTags WHERE packageName = ? AND user = ? AND version = ?", + arrayOf(pkgName, account!!.name, version.toString()) + ).use { cursor -> + while (cursor.moveToNext()) { + applicationTags.add( + ApplicationTag.Builder() + .partitionId(cursor.getLong(0)) + .tag(ByteString.of(*cursor.getBlob(1))) + .build() + ) + } + } + finskyRegularInfo.applicationTagValue(applicationTags) + db.rawQuery( + "SELECT tokensTag FROM ExperimentTokens WHERE packageName = ? AND user = ? AND version = ? AND isCommitted = 0", + arrayOf(pkgName, account.name, version.toString()) + ).use { cursor -> + if (cursor.moveToNext()) { + finskyRegularInfo.tokensTag(ByteString.of(*cursor.getBlob(0))) + } + } + val experimentsInfo = buildBaseGpInfo(FINSKY_STABLE, 0).build() + var bytesTag: ByteArray? = null + db.rawQuery( + "SELECT bytesTag FROM RequestTags WHERE user = ?", + arrayOf(account.name) + ).use { cursor -> + if (cursor.moveToNext()) { + bytesTag = cursor.getBlob(0) + } + } + checkNotNull(bytesTag) + return experimentRequestData.Builder() + .deviceDataValue(deviceData) + .experimentsInfo( + List.of( + finskyRegularInfo.build(), + experimentsInfo + ) + ) + .bytesTag(ByteString.of(*bytesTag!!)) + .actionType(action.NEW_APPLICATION_SYNC) + .unknowFieldG(128) + .expPkgName(FINSKY_STABLE) + .build() + } + } catch (e: Exception) { + Log.w(TAG, "buildRequestData: ", e) + throw RuntimeException(e) + } + } + + throw RuntimeException("request experimentsandconfigs has Unknow action") + } + + private fun createExpDeviceInfo(context: Context): ExpDeviceInfo { + @SuppressLint("HardwareIds") val builder = ExpDeviceInfo.Builder() + builder.androidId( + getString(context.contentResolver, "android_id", "")!! + .toLong() + ) + builder.sdkInt(Build.VERSION.SDK_INT) + builder.buildId(Build.ID) + builder.buildDevice(Build.DEVICE) + builder.manufacturer(Build.MANUFACTURER) + builder.model(Build.MODEL) + builder.product(Build.PRODUCT) + builder.unknowEmpty("") + builder.fingerprint(Build.FINGERPRINT) + builder.country(Locale.getDefault().country) + builder.locale(Locale.getDefault().toString()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.supportAbis(Arrays.asList(*Build.SUPPORTED_ABIS)) + } + return builder.build() + } + + private fun createDeviceData(account: Account?, context: Context): DeviceData { + val expDeviceInfo = createExpDeviceInfo(context) + val expDeviceInfoWrapper = ExpDeviceInfoWrapper.Builder() + .unknowFieldb(4) + .expDeviceInfoValue(expDeviceInfo) + .build() + return DeviceData.Builder() + .hasAccount((if (account == null) 0 else 1).toLong()) + .expDeviceInfoWrapperValue(expDeviceInfoWrapper) + .unknowFlagf(false) + .unknowEmptyE(ByteString.of()) + .unknkowFieldG(DeviceDataEmptyA.Builder().build()) + .build() + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + fun postRequest( + experimentRequestData: experimentRequestData, + context: Context?, + accountName: String, + token: String + ) { + try { + val url = + URL("https://www.googleapis.com/experimentsandconfigs/v1/getExperimentsAndConfigs" + "?r=" + experimentRequestData.actionType?.value + "&c=" + experimentRequestData.unknowFieldG) + + val httpURLConnection = url.openConnection() as HttpURLConnection + httpURLConnection.connectTimeout = 30000 + httpURLConnection.readTimeout = 30000 + httpURLConnection.doOutput = true + httpURLConnection.instanceFollowRedirects = false + httpURLConnection.setRequestProperty("Accept-Encoding", null) + httpURLConnection.setRequestProperty("Content-Type", "application/x-protobuf") + httpURLConnection.setRequestProperty("Content-Encoding", "gzip") + httpURLConnection.setRequestProperty("Authorization", "Bearer $token") + httpURLConnection.setRequestProperty( + "User-Agent", + "Android-Finsky/41.2.21-31 [0] [PR] 636997666 (api=3,versionCode=84122130,sdk=31,device=redroid_arm64,hardware=redroid,product=redroid_arm64,platformVersionRelease=12,model=redroid12_arm64,buildId=SQ1D.220205.004,isWideScreen=0,supportedAbis=arm64-v8a;armeabi-v7a;armeabi) (redroid_arm64 SQ1D.220205.004); gzip" + ) + val byteArrayOutputStream = ByteArrayOutputStream() + GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> + gzipOutputStream.write(experimentRequestData.encode()) + gzipOutputStream.finish() + } + val compressedData = byteArrayOutputStream.toByteArray() + httpURLConnection.outputStream.use { os -> + os.write(compressedData) + os.flush() + } + val responseCode = httpURLConnection.responseCode + Log.d(TAG, "postRequest responseCode: $responseCode") + if (responseCode >= 200 && responseCode < 300) { + val experimentResponseData = + ExperimentResponseData.ADAPTER.decode(toByteArray(httpURLConnection.inputStream)) + + PhenotypeDatabase(context).writableDatabase.use { db -> + if (experimentResponseData.bytesTag != null) { + db.rawQuery( + "SELECT user FROM RequestTags WHERE user = ?1", + arrayOf(accountName) + ).use { cursor -> + if (cursor.count > 0) { + db.execSQL( + "UPDATE RequestTags SET user = ?1, bytesTag = ?2 WHERE user = ?1", + arrayOf( + accountName, + experimentResponseData.bytesTag.toByteArray() + ) + ) + } else { + db.execSQL( + "INSERT INTO RequestTags (user, bytesTag) VALUES (?, ?)", + arrayOf( + accountName, + experimentResponseData.bytesTag.toByteArray() + ) + ) + } + } + } + for (experimentsDataWrapper in experimentResponseData.experiments) { + val experimentVersionValue = experimentsDataWrapper.experimentVersionValue + val pkgName = experimentVersionValue?.expPkgName + val version = experimentVersionValue?.version + for (expFlagsGroup in experimentsDataWrapper.expFlagsGroupValue) { + val partitionId = expFlagsGroup.applicationTagValue?.partitionId + + for (expFlag in expFlagsGroup.expFlags) { + var longValue: Long? = null + var booleValue: Long? = null + var doubleValue: Double? = null + var stringValue: String? = null + var extensionValue: ByteString? = "".encodeUtf8() + if (expFlag.valueType == null) continue + when (expFlag.valueType) { + 1 -> longValue = expFlag.longValue + 2 -> booleValue = if (expFlag.boolValue == true) 1L else 0L + 3 -> doubleValue = expFlag.doubleValue + 4 -> stringValue = expFlag.stringValue + 5 -> { + extensionValue = + if (expFlag.extensionValueValue == null) null else expFlag.extensionValueValue.mvalue + continue + } + + else -> continue + } + val flagType = 0 + db.execSQL( + "INSERT OR REPLACE INTO Flags(packageName, version, flagType, partitionId, user, name, committed, intVal, boolVal, floatVal, stringVal, extensionVal) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf( + pkgName, + version, + flagType, + partitionId, + accountName, + expFlag.flagName, + flagType, + longValue, + booleValue, + doubleValue, + stringValue, + extensionValue!!.toByteArray() + ) + ) + } + db.execSQL( + "DELETE FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = 0", + arrayOf(pkgName, version, accountName) + ) + db.execSQL( + "INSERT INTO ExperimentTokens (packageName, version, user, isCommitted, experimentToken, serverToken, configHash, servingVersion, tokensTag, flagsHash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf( + pkgName, + version, + accountName, + 0, + experimentsDataWrapper.experimentToken?.toByteArray(), + experimentsDataWrapper.serverToken, + calculateHash(experimentsDataWrapper).toString(), + experimentResponseData.servingVersion, + experimentsDataWrapper.tokensTag?.toByteArray(), + 0 + ) + ) + db.execSQL( + "DELETE FROM ApplicationTags WHERE packageName = ? AND version = ? AND user = ? AND partitionId = ?", + arrayOf(pkgName, version, accountName, partitionId) + ) + db.execSQL( + "INSERT OR REPLACE INTO ApplicationTags (packageName, version, partitionId, user, tag) VALUES (?, ?, ?, ?, ?)", + arrayOf( + pkgName, + version, + partitionId, + accountName, + expFlagsGroup.applicationTagValue?.tag?.toByteArray() + ) + ) + } + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + fun buildExperimentsFlag(context: Context, accountName: String, pkgName: String) { + PhenotypeDatabase(context).readableDatabase.use { database -> + var hasFlagOverrides: Boolean + database.rawQuery("SELECT EXISTS(SELECT NULL FROM FlagOverrides)", arrayOf()) + .use { cursor -> + cursor.moveToNext() + hasFlagOverrides = cursor.getInt(0) > 0 + } + var expValue: ExperimentsValues? = null + val overFlags = ArrayList() + if (hasFlagOverrides) { + database.rawQuery( + "SELECT flagType, name, intVal, boolVal, floatVal, stringVal, extensionVal FROM FlagOverrides WHERE packageName = ? AND user = \'*\' AND committed = 0", + arrayOf(pkgName) + ).use { cursor -> + while (cursor.moveToNext()) { + overFlags.add(getFlagsValue(cursor)) + } + } + for (flag in overFlags) { + if (flag!!.name == "__phenotype_server_token" && flag.valueType == 3) { + expValue = ExperimentsValues(null, flag.stringVal, 0) + } + } + } + + database.rawQuery( + "SELECT experimentToken,serverToken,servingVersion FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = 0", + arrayOf(pkgName, version.toString(), accountName) + ).use { cursor -> + cursor.moveToNext() + if (expValue == null) { + expValue = + ExperimentsValues(cursor.getBlob(0), cursor.getString(1), cursor.getLong(2)) + } + } + val flags = TreeSet() + database.rawQuery( + "SELECT flagType, name, intVal, boolVal, floatVal, stringVal, extensionVal FROM Flags WHERE packageName = ? AND version = ? AND user = ? AND committed = 0 ORDER BY name", + arrayOf(pkgName, version.toString(), accountName) + ).use { cursor -> + while (cursor.moveToNext()) { + flags.add(getFlagsValue(cursor)) + } + } + for (flagsValue in overFlags) { + flags.remove(flagsValue) + flags.add(flagsValue) + } + val flagsValueMap = HashMap?>() + for (flag in flags) { + if (flagsValueMap[flag!!.flagType] == null) flagsValueMap[flag.flagType] = + ArrayList() + Objects.requireNonNull(flagsValueMap[flag.flagType])!!.add(flag) + } + val flagTypeList = ArrayList() + for (flagType in flagsValueMap.keys) { + flagTypeList.add( + FlagTypeValue( + flagType, Objects.requireNonNull( + flagsValueMap[flagType] + )!!.toTypedArray() + ) + ) + } + + commit(database, pkgName, accountName) + database.rawQuery( + "SELECT configHash FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = ?", + arrayOf(pkgName, version.toString(), accountName, "1") + ).use { cursor -> + cursor.moveToNext() + val configHash = cursor.getString(0) + val expeIntroduce = "$pkgName $accountName $configHash" + + val configuration = ExperimentsFlagsConfiguration( + expeIntroduce, + expValue!!.serverToken, + flagTypeList.toTypedArray(), + false, + expValue!!.experimentToken, + expValue!!.servingVersion + ) + val ExperimentsFlagsProto = buildExperimentsFlagsProto(configuration) + writeExperimentsFlag(ExperimentsFlagsProto, context, pkgName, accountName) + } + } + } + + private fun commit(database: SQLiteDatabase, pkgName: String, accountName: String) { + database.execSQL( + "INSERT OR REPLACE INTO ExperimentTokens SELECT packageName, version, user, 1 AS isCommitted, experimentToken, serverToken, configHash, servingVersion, tokensTag, flagsHash FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = 0", + arrayOf(pkgName, version.toString(), accountName) + ) + database.execSQL( + "DELETE FROM Flags WHERE packageName = ? AND committed = 1", + arrayOf(pkgName) + ) + database.execSQL( + "INSERT INTO Flags SELECT packageName, version, flagType, partitionId, user, name, intVal, boolVal, floatVal, stringVal, extensionVal, 1 AS committed FROM Flags WHERE packageName = ? AND version = ? AND user = ? AND committed = 0", + arrayOf(pkgName, version.toString(), accountName) + ) + } + + @JvmStatic + fun readExperimentsFlag( + context: Context, + pkgName: String, + username: String? + ): ExperimentsDataRead? { + val file = File( + context.filesDir, + if (FINSKY_REGULAR == pkgName) (if (TextUtils.isEmpty(username)) "experiment-flags-regular-null-account" else "experiment-flags-regular-" + Uri.encode( + username + )) else "experiment-flags-process-stable" + ) + if (!file.exists()) { + Log.d(TAG, "File " + file.name + " not exists") + return null + } + try { + val inputStream = DataInputStream(BufferedInputStream(FileInputStream(file))) + if (inputStream.readByte().toInt() != 1) { + throw IOException("Unrecognized file version.") + } + val result = ExperimentsDataRead() + result.setBaseToken( + inputStream.readUTF(), inputStream.readUTF(), ExperimentTokenStore.ADAPTER.decode( + Base64.decode(inputStream.readUTF(), 3) + ) + ) + var endOfFlag = 0 + while (endOfFlag == 0) { + when (inputStream.readByte()) { + 0.toByte() -> endOfFlag = 1 + 1.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readByte().toLong()) + 2.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readShort().toLong()) + 3.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readInt().toLong()) + 4.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readLong()) + 5.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readUTF()) + 6.toByte() -> { + val key = inputStream.readUTF() + val length = inputStream.readInt() + if (length >= 0) { + val value = ByteArray(length) + inputStream.readFully(value) + result.putFlag(key, value) + break + } + throw RuntimeException("Bytes flag has negative length.") + } + + 7.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readDouble()) + 8.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readBoolean()) + else -> throw RuntimeException("Unknown flag type") + } + } + inputStream.close() + return result + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + private fun writeExperimentsFlag( + ExperimentsFlagsProto: ExperimentsFlagsProto, + context: Context, + pkgName: String, + username: String + ) { + try { + val file = File( + context.filesDir, + if (FINSKY_REGULAR == pkgName) (if (TextUtils.isEmpty(username)) "experiment-flags-regular-null-account" else "experiment-flags-regular-" + Uri.encode( + username + )) else "experiment-flags-process-stable" + ) + val dataOutputStream = DataOutputStream(BufferedOutputStream(FileOutputStream(file))) + dataOutputStream.writeByte(1) + dataOutputStream.writeUTF(ExperimentsFlagsProto.serverToken) + dataOutputStream.writeUTF(ExperimentsFlagsProto.expeIntroduce) + dataOutputStream.writeUTF( + Base64.encodeToString( + buildExperimentsTokenProto( + context, + username, + pkgName + ).encode(), 3 + ) + ) + for (flag in ExperimentsFlagsProto.flagValues) { + if (flag.intVal != null) { + val value = flag.intVal + if (value >= -0x80L && value <= 0x7FL) { + dataOutputStream.writeByte(1) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeByte((value.toInt())) + } else if (value >= -0x8000L && value <= 0x7FFFL) { + dataOutputStream.writeByte(2) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeShort((value.toInt())) + } else if (value >= -0x80000000L && value <= 0x7FFFFFFFL) { + dataOutputStream.writeByte(3) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeInt((value.toInt())) + } else { + dataOutputStream.writeByte(4) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeLong(value) + } + } else if (flag.boolVal != null) { + dataOutputStream.writeByte(8) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeBoolean(flag.boolVal) + } else if (flag.floatVal != null) { + dataOutputStream.writeByte(7) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeDouble(flag.floatVal) + } else if (flag.stringVal != null) { + dataOutputStream.writeByte(5) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeUTF(flag.stringVal) + } else if (flag.extensionVal != null) { + dataOutputStream.writeByte(6) + dataOutputStream.writeUTF(flag.name) + dataOutputStream.writeInt(flag.extensionVal.size) + dataOutputStream.write( + flag.extensionVal.toByteArray(), + 0, + flag.extensionVal.size + ) + } + } + Log.d(TAG, "Finished writing experiment flags into file " + file.name) + dataOutputStream.writeByte(0) + dataOutputStream.close() + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + private fun buildExperimentsTokenProto( + context: Context, + user: String, + pkgName: String + ): ExperimentTokenStore { + PhenotypeDatabase(context).readableDatabase.use { db -> + db.rawQuery( + "SELECT experimentToken FROM ExperimentTokens WHERE user = ? AND packageName = ? AND version = ? AND isCommitted = 1", + arrayOf(user, pkgName, version.toString()) + ).use { cursor -> + cursor.moveToNext() + val ExperimentTokenStore_ = ExperimentTokenStore.Builder() + ExperimentTokenStore_.experimentToken = + Arrays.asList(ByteString.of(*cursor.getBlob(0))) + return ExperimentTokenStore_.build() + } + } + } + + private fun buildExperimentsFlagsProto(configuration: ExperimentsFlagsConfiguration): ExperimentsFlagsProto { + val builder = ExperimentsFlagsProto.Builder() + .expeIntroduce(configuration.expeIntroduce) + .serverToken(configuration.serverToken) + .unknowFlagB(configuration.unknowFlagB) + .servingVersion(configuration.servingVersion) + if (configuration.experimentToken != null) { + builder.experimentToken(ByteString.of(*configuration.experimentToken)) + } + val flagValueProtos = builder.flagValues.toMutableList() + for (typeValue in configuration.array) { + for (flagsValue in typeValue.values) { + flagValue2proto(flagsValue)?.let { flagValueProtos.add(it) } + } + } + builder.flagValues = flagValueProtos + return builder.build() + } + + private fun flagValue2proto(value: FlagsValue?): FlagValueProto? { + when (value!!.valueType) { + 0 -> return FlagValueProto.Builder() + .name(value.name) + .intVal(value.intVal.toLong()).build() + + 1 -> return FlagValueProto.Builder() + .name(value.name) + .boolVal(value.boolVal).build() + + 2 -> return FlagValueProto.Builder() + .name(value.name) + .floatVal(value.floatVal.toDouble()).build() + + 3 -> return FlagValueProto.Builder() + .name(value.name) + .stringVal(value.stringVal).build() + + 4 -> return FlagValueProto.Builder() + .name(value.name) + .extensionVal(ByteString.of(*value.extensionVal?:byteArrayOf())).build() + } + return null + } + + private fun getFlagsValue(cursor: Cursor): FlagsValue? { + val flagType = cursor.getInt(0) + val name = cursor.getString(1) + if (!cursor.isNull(2)) { + return FlagsValue(flagType, name, cursor.getInt(2)) + } else if (!cursor.isNull(3)) { + return FlagsValue(flagType, name, cursor.getInt(3) != 0) + } else if (!cursor.isNull(4)) { + return FlagsValue(flagType, name, cursor.getFloat(4)) + } else if (!cursor.isNull(5)) { + return FlagsValue(flagType, name, cursor.getString(5)) + } else if (!cursor.isNull(6)) { + return FlagsValue(flagType, name, cursor.getString(6)) + } + return null + } + + private fun calculateHash(experimentsDataWrapper: ExperimentsDataWrapper): Int { + var hash = 0 + for (expFlagsGroup in experimentsDataWrapper.expFlagsGroupValue) { + var applicationTag = expFlagsGroup.applicationTagValue + if (applicationTag == null) applicationTag = ApplicationTag.Builder().build() + var hashCode = applicationTag!!.partitionId.hashCode() + for (b in applicationTag.tag!!.toByteArray()) { + hashCode = hashCode * 0x1F + b + } + hash = hash * 17 xor hashCode + } + return hash + } + + @JvmStatic + fun toByteArray(inputStream: InputStream): ByteArray { + val buffer = ByteArrayOutputStream() + var nRead: Int + val data = ByteArray(1024) + + while ((inputStream.read(data, 0, data.size).also { nRead = it }) != -1) { + buffer.write(data, 0, nRead) + } + buffer.flush() + return buffer.toByteArray() + } + + + class ExperimentsDataRead { + @JvmField + var serverToken: String? = null + var expeIntroduce: String? = null + var experimentToken: ExperimentTokenStore? = null + val flagMap: MutableMap = HashMap() + + fun setBaseToken( + serverToken: String?, + expeIntroduce: String?, + experimentToken: ExperimentTokenStore? + ) { + this.serverToken = serverToken + this.expeIntroduce = expeIntroduce + this.experimentToken = experimentToken + } + + fun putFlag(name: String, value: Boolean) { + flagMap[name] = value + } + + fun putFlag(name: String, value: Long) { + flagMap[name] = value + } + + fun putFlag(name: String, value: Double) { + flagMap[name] = value + } + + fun putFlag(name: String, value: String) { + flagMap[name] = value + } + + fun putFlag(name: String, value: ByteArray) { + flagMap[name] = value + } + } + + internal class ExperimentsFlagsConfiguration( + val expeIntroduce: String, + val serverToken: String?, + val array: Array, + val unknowFlagB: Boolean, + val experimentToken: ByteArray?, + val servingVersion: Long + ) + + internal class FlagTypeValue(private val flagType: Int, val values: Array) + + internal class ExperimentsValues( + var experimentToken: ByteArray?, + var serverToken: String?, + var servingVersion: Long + ) + + class FlagsValue : Comparable { + val flagType: Int + val name: String + var intVal: Int = 0 + var boolVal: Boolean = false + var floatVal: Float = 0f + var stringVal: String? = null + var extensionVal: ByteArray? = null + var valueType: Int + + constructor(flagType: Int, name: String, intVal: Int) { + this.valueType = 0 + this.flagType = flagType + this.name = name + this.intVal = intVal + } + + constructor(flagType: Int, name: String, boolVal: Boolean) { + this.valueType = 1 + this.flagType = flagType + this.name = name + this.boolVal = boolVal + } + + constructor(flagType: Int, name: String, floatVal: Float) { + this.valueType = 2 + this.flagType = flagType + this.name = name + this.floatVal = floatVal + } + + constructor(flagType: Int, name: String, stringVal: String?) { + this.valueType = 3 + this.flagType = flagType + this.name = name + this.stringVal = stringVal + } + + override fun compareTo(flagValue: FlagsValue?): Int { + if (flagValue == null) { + return -1 + } + return name.compareTo(flagValue?.name!!) + } + + val value: Any? + get() { + when (this.valueType) { + 0 -> return intVal + 1 -> return boolVal + 2 -> return floatVal + 3 -> return stringVal + 4 -> return extensionVal + } + return null + } + + override fun equals(obj: Any?): Boolean { + if (this.valueType == (obj as FlagsValue?)!!.valueType) { + when (this.valueType) { + 0 -> return this.intVal == obj!!.intVal + 1 -> return this.boolVal == obj!!.boolVal + 2 -> return this.floatVal == obj!!.floatVal + 3 -> return this.stringVal == obj!!.stringVal + 4 -> return this.extensionVal == obj!!.extensionVal + } + } + return false + } + } +} diff --git a/vending-app/src/main/kotlin/com/android/vending/MainApplication.kt b/vending-app/src/main/kotlin/com/android/vending/MainApplication.kt new file mode 100644 index 0000000000..4ab2766dc4 --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/MainApplication.kt @@ -0,0 +1,93 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.android.vending + +import android.accounts.AccountManager +import android.accounts.AuthenticatorException +import android.accounts.OperationCanceledException +import android.app.ActivityManager +import android.app.Application +import android.content.Context +import android.os.Build +import android.os.Process +import android.util.Log +import com.google.android.phonesky.header.PayloadsProtoStore +import com.google.android.phonesky.header.PhoneskyHeaderValue +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE +import org.microg.vending.billing.FINSKY_REGULAR +import org.microg.vending.billing.FINSKY_STABLE +import java.io.IOException +import java.util.concurrent.TimeUnit + +class MainApplication : Application() { + override fun onCreate() { + super.onCreate() + + val accountManager = AccountManager.get(this) + val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) + + if (isMainProcess() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && accounts.isNotEmpty()) { + GlobalScope.launch(Dispatchers.IO) { + val payloads = PayloadsProtoStore.readCache(applicationContext) + if (payloads == null || payloads.mvalue.isEmpty()) PayloadsProtoStore.cachePayload(accounts[0], applicationContext) + for (account in accounts) { + var token : String? + val accountName = account.name + val future = accountManager.getAuthToken( + account, + "oauth2:https://www.googleapis.com/auth/experimentsandconfigs", + null, + false, + null, + null + ) + try { + val bundle = future.getResult(2, TimeUnit.SECONDS) + token = bundle.getString(AccountManager.KEY_AUTHTOKEN) + } catch (e: Exception) { + return@launch + } + + if (token == null) { + Log.d("MainApplication", "onCreate token is null") + return@launch + } + + ExperimentAndConfigs.postRequest( + ExperimentAndConfigs.buildRequestData(this@MainApplication), this@MainApplication, accountName, token) + ExperimentAndConfigs.postRequest( + ExperimentAndConfigs.buildRequestData(this@MainApplication, "NEW_APPLICATION_SYNC", FINSKY_REGULAR, account), this@MainApplication, accountName, token) + ExperimentAndConfigs.postRequest( + ExperimentAndConfigs.buildRequestData(this@MainApplication, "NEW_USER_SYNC_ALL_ACCOUNT", null, null), this@MainApplication, "", token) + ExperimentAndConfigs.buildExperimentsFlag(this@MainApplication, accountName, FINSKY_REGULAR) + ExperimentAndConfigs.buildExperimentsFlag(this@MainApplication, "", FINSKY_REGULAR) + ExperimentAndConfigs.buildExperimentsFlag(this@MainApplication, accountName, FINSKY_STABLE) + } + try { + PhoneskyHeaderValue.getPhoneskyHeader(this@MainApplication, accounts[0]) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: IllegalAccessException) { + throw RuntimeException(e) + } catch (e: AuthenticatorException) { + throw RuntimeException(e) + } catch (e: OperationCanceledException) { + throw RuntimeException(e) + } + } + } + } + + private fun isMainProcess(): Boolean { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processInfo = am.runningAppProcesses ?: return false + val mainProcessName = packageName + val myPid = Process.myPid() + return processInfo.any { it.pid == myPid && it.processName == mainProcessName } + } +} diff --git a/vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt b/vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt new file mode 100644 index 0000000000..f6d3350cf0 --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt @@ -0,0 +1,58 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.android.vending + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteOpenHelper + +class PhenotypeDatabase(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { + + companion object { + const val DATABASE_NAME = "phenotype.db" + const val DATABASE_VERSION = 0x20 + } + + override fun onCreate(db: SQLiteDatabase) { + db.execSQL(" CREATE TABLE IF NOT EXISTS Packages(\n packageName TEXT NOT NULL PRIMARY KEY,\n version INTEGER NOT NULL,\n params BLOB,\n dynamicParams BLOB,\n weak INTEGER NOT NULL,\n androidPackageName TEXT NOT NULL,\n isSynced INTEGER,\n serializedDeclarativeRegInfo BLOB DEFAULT NULL,\n configTier INTEGER DEFAULT NULL,\n baselineCl INTEGER DEFAULT NULL,\n heterodyneInfo BLOB DEFAULT NULL,\n runtimeProperties BLOB DEFAULT NULL,\n declarativeRegistrationInfo BLOB DEFAULT NULL\n )\n") + db.execSQL("CREATE INDEX IF NOT EXISTS androidPackageName ON Packages (androidPackageName)") + db.execSQL(" CREATE TABLE IF NOT EXISTS ApplicationStates(\n packageName TEXT NOT NULL PRIMARY KEY,\n user TEXT NOT NULL,\n version INTEGER NOT NULL,\n patchable INTEGER\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS MultiCommitApplicationStates(\n packageName TEXT NOT NULL,\n user TEXT NOT NULL,\n version INTEGER NOT NULL,\n PRIMARY KEY(packageName, user)\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS LogSources(\n logSourceName TEXT NOT NULL,\n packageName TEXT NOT NULL,\n PRIMARY KEY(logSourceName, packageName)\n )\n") + db.execSQL("CREATE INDEX IF NOT EXISTS packageName ON LogSources(packageName)") + db.execSQL(" CREATE TABLE IF NOT EXISTS WeakExperimentIds(\n packageName TEXT NOT NULL,\n experimentId INTEGER NOT NULL\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS ExperimentTokens(\n packageName TEXT NOT NULL,\n version INTEGER NOT NULL,\n user TEXT NOT NULL,\n isCommitted INTEGER NOT NULL,\n experimentToken BLOB NOT NULL,\n serverToken TEXT NOT NULL,\n configHash TEXT NOT NULL DEFAULT \'\',\n servingVersion INTEGER NOT NULL DEFAULT 0,\n tokensTag BLOB DEFAULT NULL,\n flagsHash INTEGER DEFAULT NULL,\n PRIMARY KEY(packageName, version, user, isCommitted)\n )\n") + db.execSQL("CREATE INDEX IF NOT EXISTS committed ON ExperimentTokens(packageName, version, user, isCommitted)") + db.execSQL(" CREATE TABLE IF NOT EXISTS ExternalExperimentTokens(\n packageName TEXT NOT NULL PRIMARY KEY,\n experimentToken BLOB NOT NULL\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS Flags(\n packageName TEXT NOT NULL,\n version INTEGER NOT NULL,\n flagType INTEGER NOT NULL,\n partitionId INTEGER NOT NULL,\n user TEXT NOT NULL,\n name TEXT NOT NULL,\n intVal INTEGER,\n boolVal INTEGER,\n floatVal REAL,\n stringVal TEXT,\n extensionVal BLOB,\n committed INTEGER NOT NULL,\n PRIMARY KEY(packageName, version, flagType, partitionId, user, name, committed)\n );\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS RequestTags(\n user TEXT NOT NULL PRIMARY KEY,\n bytesTag BLOB NOT NULL\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS ApplicationTags(\n packageName TEXT NOT NULL,\n version INTEGER NOT NULL,\n partitionId INTEGER NOT NULL,\n user TEXT NOT NULL,\n tag BLOB NOT NULL,\n PRIMARY KEY(packageName, version, partitionId, user)\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS CrossLoggedExperimentTokens(\n fromPackageName TEXT NOT NULL,\n fromVersion INTEGER NOT NULL,\n fromUser TEXT NOT NULL,\n toPackageName TEXT NOT NULL,\n toVersion INTEGER NOT NULL,\n isCommitted INTEGER NOT NULL,\n token BLOB NOT NULL,\n provenance INTEGER NOT NULL\n )\n") + db.execSQL(" CREATE INDEX IF NOT EXISTS apply ON CrossLoggedExperimentTokens(\n fromPackageName,\n fromVersion,\n fromUser,\n toPackageName,\n toVersion,\n isCommitted\n )\n") + db.execSQL("CREATE INDEX IF NOT EXISTS remove ON CrossLoggedExperimentTokens(toPackageName)") + db.execSQL(" CREATE TABLE IF NOT EXISTS ChangeCounts(\n packageName TEXT NOT NULL PRIMARY KEY,\n count INTEGER NOT NULL\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS DogfoodsToken(\n \"key\" INTEGER NOT NULL PRIMARY KEY,\n token BLOB\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS LastFetch(\n \"key\" INTEGER NOT NULL PRIMARY KEY,\n servertimestamp INTEGER NOT NULL\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS FlagOverrides(\n packageName TEXT NOT NULL,\n user TEXT NOT NULL,\n name TEXT NOT NULL,\n flagType INTEGER NOT NULL,\n intVal INTEGER,\n boolVal INTEGER,\n floatVal REAL,\n stringVal TEXT,\n extensionVal BLOB,\n committed,\n PRIMARY KEY(packageName, user, name, committed)\n );\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS LastSyncAfterRequest(\n packageName TEXT NOT NULL PRIMARY KEY,\n servingVersion INTEGER NOT NULL DEFAULT 0,\n androidPackageName TEXT DEFAULT NULL\n )\n") + db.execSQL(" CREATE TABLE IF NOT EXISTS StorageInfos (\n androidPackageName TEXT UNIQUE NOT NULL,\n secret BLOB NOT NULL,\n deviceEncryptedSecret BLOB NOT NULL\n )\n") + db.execSQL(" CREATE TABLE AppWideProperties (\n androidPackageName TEXT UNIQUE NOT NULL,\n appWideProperties BLOB NOT NULL\n );\n") + } + + override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + } + + override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + super.onDowngrade(db, oldVersion, newVersion) + db.execSQL("DROP TABLE IF EXISTS android_packages;") + db.execSQL("DROP TABLE IF EXISTS config_packages;") + db.execSQL("DROP TABLE IF EXISTS config_packages_to_log_sources;") + db.execSQL("DROP TABLE IF EXISTS cross_logged_tokens;") + db.execSQL("DROP TABLE IF EXISTS flag_overrides;") + db.execSQL("DROP TABLE IF EXISTS log_sources;") + db.version = newVersion + db.setForeignKeyConstraintsEnabled(true) + } +} 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 new file mode 100644 index 0000000000..defec77a7d --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.google.android.finsky.splitinstallservice + +import android.content.Intent +import android.os.IBinder +import androidx.lifecycle.LifecycleService +import com.google.android.play.core.splitinstall.protocol.ISplitInstallService +import org.microg.gms.profile.ProfileManager + +class SplitInstallService : LifecycleService() { + private var mService: ISplitInstallService? = null + + override fun onCreate() { + super.onCreate() + ProfileManager.ensureInitialized(this) + } + + override fun onBind(intent: Intent): IBinder? { + if (mService == null) { + mService = SplitInstallServiceImpl(this.applicationContext) + } + return mService as IBinder? + } +} 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 new file mode 100644 index 0000000000..6fd2850d26 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallServiceImpl.kt @@ -0,0 +1,451 @@ +/** + * 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.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.RemoteException +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import com.android.vending.R +import com.google.android.phonesky.header.PhoneskyHeaderValue.GoogleApiRequest +import com.google.android.phonesky.header.PhoneskyValue +import com.google.android.phonesky.header.RequestLanguagePkg +import com.google.android.play.core.splitinstall.protocol.ISplitInstallService +import com.google.android.play.core.splitinstall.protocol.ISplitInstallServiceCallback +import org.brotli.dec.BrotliInputStream +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.IOException +import java.io.InputStream +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.URL +import java.nio.file.Files +import java.util.concurrent.BlockingQueue +import java.util.concurrent.LinkedBlockingQueue + +class SplitInstallServiceImpl(private val context: Context) : ISplitInstallService.Stub(){ + + 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.j(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.i(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.d(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") + } + + @RequiresApi(api = Build.VERSION_CODES.N) + 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") + } + + @SuppressLint("StringFormatMatches") + 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 + var versionCode: Long = 0 + try { + val packageInfo = packageManager.getPackageInfo(packageName, 0) + versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode // For API level 28 and above + } else { + packageInfo.versionCode.toLong() // For API level 27 and below + } + } catch (e: PackageManager.NameNotFoundException) { + Log.e("SplitInstallServiceImpl", "Error getting package info", e) + } + 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 { + val downloadTempFile = File(context.filesDir, "phonesky-download-service/temp") + if (!downloadTempFile.parentFile.exists()) { + downloadTempFile.parentFile.mkdirs() + } + val language:String? = if (langName.isNotEmpty()) { + langName[0] + } else { + null + } + for (downloadUrl in downloadUrls) { + taskQueue.put(Runnable { + installSplitPackage(downloadUrl, downloadTempFile, packageName, language) + }) + } + + return true + } catch (e: Exception) { + Log.e("SplitInstallServiceImpl", "Error downloading split", e) + return false + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun downloadSplitPackage(downloadUrl: String, downloadTempFile: File) : Boolean{ + Log.d(TAG, "downloadSplitPackage downloadUrl:$downloadUrl") + val url = URL(downloadUrl) + val connection = url.openConnection() as HttpURLConnection + connection.readTimeout = 30000 + connection.connectTimeout = 30000 + connection.requestMethod = "GET" + if (connection.responseCode == HttpURLConnection.HTTP_OK) { + val tempFile: OutputStream = + BufferedOutputStream(Files.newOutputStream(downloadTempFile.toPath())) + val inputStream: InputStream = BufferedInputStream(connection.inputStream) + val buffer = ByteArray(4096) + var bytesRead: Int + + while ((inputStream.read(buffer).also { bytesRead = it }) != -1) { + Log.d(TAG, "downloadSplitPackage: $bytesRead") + tempFile.write(buffer, 0, bytesRead) + } + inputStream.close() + tempFile.close() + } + Log.d(TAG, "downloadSplitPackage code: " + connection.responseCode) + return connection.responseCode == HttpURLConnection.HTTP_OK + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun installSplitPackage( + downloadUrl: String, + downloadTempFile: File, + packageName: String, + language: String? + ) { + try { + Log.d(TAG, "installSplitPackage downloadUrl:$downloadUrl") + if (downloadSplitPackage(downloadUrl, downloadTempFile)) { + 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 + try { + sessionId = packageInstaller.createSession(params) + val session = packageInstaller.openSession(sessionId) + try { + val buffer = ByteArray(4096) + var bytesRead: Int + BrotliInputStream(FileInputStream(downloadTempFile)).use { `in` -> + session.openWrite("MGSplitPackage", 0, -1).use { out -> + while ((`in`.read(buffer).also { bytesRead = it }) != -1) { + out.write(buffer, 0, bytesRead) + } + session.fsync(out) + } + } + } 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) + 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) + } + } + } 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, + PhoneskyValue.Builder().languages( + RequestLanguagePkg.Builder().language(langName.filterNotNull()).build() + ).build() + ) + val response = googleApiRequest.sendRequest(null) + val pkgs = response?.fdfeApiResponseValue?.splitReqResult?.pkgList?.pkgDownlaodInfo + if (pkgs != null) { + for (item in pkgs) { + for (lang in langName) { + if (("config.$lang") == item.splitPkgName) { + downloadUrls.add(item.slaveDownloadInfo!!.url!!) + } + } + Log.d(TAG, "requestSplitsPackage: $splitName") + for (split in splitName) { + if (split != null && split == item.splitPkgName) { + downloadUrls.add(item.slaveDownloadInfo!!.url!!) + } + } + } + } + } 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()) { + taskQueue.take().run() + } + if(taskQueue.size <= 1) NotificationManagerCompat.from(context).cancel(1) + sendCompleteBroad(context, intent.getStringExtra("pkg")?:"", intent.getStringExtra("language")) + } + + PackageInstaller.STATUS_FAILURE -> { + taskQueue.clear(); + val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + Log.d("InstallResultReceiver", errorMsg ?: "") + } + + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + val intent0 = + intent.extras!![Intent.EXTRA_INTENT] as Intent? + intent0!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ContextCompat.startActivity(context, intent0, null) + } + + else -> { + taskQueue.clear() + NotificationManagerCompat.from(context).cancel(1) + Log.w(TAG, "onReceive: install fail") + } + } + } catch (e: Exception) { + taskQueue.clear() + NotificationManagerCompat.from(context).cancel(1) + Log.w(TAG, "Error handling install result", e) + } + } + + private fun sendCompleteBroad(context: Context, pkg: String, split: String?) { + Log.d(TAG, "sendCompleteBroad: $pkg") + val extra = Bundle() + extra.putInt("status", 5) + extra.putLong("total_bytes_to_download", 99999) + extra.putString("languages", split) + extra.putInt("error_code", 0) + extra.putInt("session_id", 0) + extra.putLong("bytes_downloaded", 99999) + val intent = Intent("com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService") + intent.setPackage(pkg) + intent.putExtra("session_state", extra) + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + context.sendBroadcast(intent) + } + } + + 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/phonesky/header/PayloadsProtoStore.kt b/vending-app/src/main/kotlin/com/google/android/phonesky/header/PayloadsProtoStore.kt new file mode 100644 index 0000000000..90e02aac8c --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/phonesky/header/PayloadsProtoStore.kt @@ -0,0 +1,777 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +package com.google.android.phonesky.header + +import android.accounts.Account +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.app.ActivityManager +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.graphics.Point +import android.opengl.GLES10 +import android.os.Build +import android.telephony.TelephonyManager +import android.text.TextUtils +import android.util.Base64 +import android.util.DisplayMetrics +import android.util.Log +import android.view.WindowManager +import org.microg.vending.billing.GServices.getString +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.util.Arrays +import java.util.Objects +import java.util.Random +import java.util.regex.Pattern +import java.util.stream.Collectors +import javax.microedition.khronos.egl.EGL10 +import javax.microedition.khronos.egl.EGLConfig +import javax.microedition.khronos.egl.EGLContext +import javax.microedition.khronos.egl.EGLDisplay +import javax.microedition.khronos.egl.EGLSurface + +object PayloadsProtoStore { + private val TAG: String = PayloadsProtoStore::class.java.simpleName + private const val FILE_NAME = "finsky/shared/payload_valuestore.pb" + + fun accountSha256(account: Account, context: Context): String? { + try { + val androidId = getString(context.contentResolver, "android_id", "") + val androidIdAcc = (androidId + "-" + account.name).toByteArray() + val messageDigest0 = MessageDigest.getInstance("SHA256") + messageDigest0.update(androidIdAcc, 0, androidIdAcc.size) + return Base64.encodeToString(messageDigest0.digest(), 11) + } catch (ignored: Exception) { + return null + } + } + + @JvmStatic + fun readCache(context: Context): SyncReqWrapper? { + Log.d(TAG, "readCache: ") + val cacheFile = File(context.filesDir, FILE_NAME) + if (!cacheFile.exists()) { + return null + } + try { + FileInputStream(cacheFile).use { inputStream -> + return SyncReqWrapper.ADAPTER.decode(inputStream) + } + } catch (e: IOException) { + Log.w(TAG, "Error reading person from file", e) + return null + } + } + + fun cachePayload(account: Account, context: Context) { + Log.d(TAG, "cachePayload: ") + val builder = SyncReqWrapper.Builder() + val payloads = buildPayloads(account, context) + for (payload in payloads) { + if (payload != null) { + builder.mvalue = builder.mvalue.toMutableList().apply { add(payload) } + } + } + val cacheFile = File(context.filesDir, FILE_NAME) + try { + if (!cacheFile.exists()) { + if (!cacheFile.parentFile.exists()) cacheFile.parentFile.mkdirs() + cacheFile.createNewFile() + } + } catch (e: Exception) { + Log.w(TAG, "Create payload_valuestore.pb failed !") + return + } + try { + FileOutputStream(cacheFile).use { outputStream -> + outputStream.write(builder.build().encode()) + Log.d(TAG, "Person written to file: " + cacheFile.absolutePath) + } + } catch (e: IOException) { + Log.w(TAG, "Error writing person to file", e) + } + } + + private fun generateRandomIMEI(): String { + val random = Random() + + // Generate the first 14 random digits + val imeiBuilder = StringBuilder() + for (i in 0..13) { + val digit = random.nextInt(10) + imeiBuilder.append(digit) + } + + // Calculate the check digit + val imei14 = imeiBuilder.toString() + val checkDigit = calculateLuhnCheckDigit(imei14) + + // Splice into a complete IMEI + imeiBuilder.append(checkDigit) + return imeiBuilder.toString() + } + + private fun calculateLuhnCheckDigit(imei14: String): Int { + var sum = 0 + for (i in 0 until imei14.length) { + var digit = Character.getNumericValue(imei14[i]) + if (i % 2 == 1) { + digit *= 2 + } + if (digit > 9) { + digit -= 9 + } + sum += digit + } + return (10 - (sum % 10)) % 10 + } + + private fun buildPayloads(account: Account, context: Context): Array { + val gpuInfos: ArrayList = fetchGLInfo() ?: return arrayOfNulls(0) + //---------------------------------------GPU info-------------------------------------------------------------------- + val accountSha256 = accountSha256(account, context) + val accountAssValue = AccountAssValue.Builder().mvalue(accountSha256).build() + val accountAossiationPayload = + AccountAossiationPayload.Builder().mvalue(accountAssValue).build() + val accountAossiationPayloadRequest = + SyncRequest.Builder().AccountAossiationPayloadVALUE(accountAossiationPayload).build() + //-------------------------------------------------------------------------------------------------------------------- + val carrierPropertiesPayloadRequest = createCarrierPropertiesPayloadRequest(context) + + val deviceAccountsPayloadRequest = createDeviceAccountsPayloadRequest(context) + + val deviceInfoCollect = createDeviceInfoCollect(context, gpuInfos.filterNotNull()) + + val deviceCapabilitiesPayloadRequest = + createDeviceCapabilitiesPayloadRequest(deviceInfoCollect) + + val deviceInputPropertiesPayloadRequest = + createDeviceInputPropertiesPayloadRequest(deviceInfoCollect) + + val deviceModelPayloadRequest = createDeviceModelPayloadRequest() + + val enterprisePropertiesPayloadRequest = createEnterprisePropertiesPayloadRequest(context) + + val hardwareIdentifierPayloadRequest = createHardwareIdentifierPayloadRequest(context) + + val hardwarePropertiesPayloadRequest = + createHardwarePropertiesPayloadRequest(deviceInfoCollect) + + val localePropertiesPayloadRequest = createLocalePropertiesPayloadRequest() + + val playPartnerPropertiesPayloadRequest = createPlayPartnerPropertiesPayloadRequest() + + val playPropertiesPayloadRequest = createPlayPropertiesPayload(context) + + val screenPropertiesPayloadRequest = createScreenPropertiesPayloadRequest(deviceInfoCollect) + + val systemPropertiesPayloadRequest = createSystemPropertiesPayloadRequest(deviceInfoCollect) + + val gpuPayloadRequest = createGpuPayloadRequest(gpuInfos.filterNotNull()) + + return arrayOf( + accountAossiationPayloadRequest, + carrierPropertiesPayloadRequest, + deviceAccountsPayloadRequest, + deviceCapabilitiesPayloadRequest, + deviceInputPropertiesPayloadRequest, + deviceModelPayloadRequest, + enterprisePropertiesPayloadRequest, + hardwareIdentifierPayloadRequest, + hardwarePropertiesPayloadRequest, + localePropertiesPayloadRequest, // NOTIFICATION_ROUTING_INFO_PAYLOAD, + playPartnerPropertiesPayloadRequest, + playPropertiesPayloadRequest, + screenPropertiesPayloadRequest, + systemPropertiesPayloadRequest, + gpuPayloadRequest + ) + } + + private fun createCarrierPropertiesPayloadRequest(context: Context): SyncRequest? { + var carrierPropertiesPayloadRequest: SyncRequest? = null + try { + val telephonyManager = + context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + @SuppressLint("HardwareIds") val subscriberId1 = + (telephonyManager.subscriberId.toLong() / 100000L).toString() + "00000" + val groupIdLevel = telephonyManager.groupIdLevel1 + val simOperator = telephonyManager.simOperator + val operatorName = telephonyManager.simOperatorName + var simcardId = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + simcardId = telephonyManager.simCarrierId + } + var carrierIdFromSimMccMnc = 0 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + carrierIdFromSimMccMnc = telephonyManager.carrierIdFromSimMccMnc + } + + val telephonyInfo = TelephonyInfo.Builder().subscriberId1(subscriberId1.toLong()) + .operatorName(operatorName).groupidLevel(groupIdLevel).simcardId(simcardId) + .CarrierIdFromSimMccMnc(carrierIdFromSimMccMnc).build() + + val telephonyStateWrapper = + TelephonyStateWrapper.Builder().mvalue(telephonyInfo).build() + val carrierPropertiesPayload = CarrierPropertiesPayload.Builder() + .telephonyStateValue(telephonyStateWrapper).simOperator(simOperator).build() + carrierPropertiesPayloadRequest = + SyncRequest.Builder().CarrierPropertiesPayloadVALUE(carrierPropertiesPayload) + .build() + } catch (securityException: SecurityException) { + Log.w(TAG, "SecurityException when reading IMSI.", securityException) + } catch (stateException: IllegalStateException) { + Log.w( + TAG, + "IllegalStateException when reading IMSI. This is a known SDK 31 Samsung bug.", + stateException + ) + } + return carrierPropertiesPayloadRequest + } + + private fun createDeviceAccountsPayloadRequest(context: Context): SyncRequest { + val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager + val accounts = accountManager.accounts + + val builder = DeviceAccountsPaylaod.Builder() + for (account in accounts) { + builder.mvalue = builder.mvalue.toMutableList().apply { + add( + AccountAssValue.Builder().mvalue(accountSha256(account, context)).build() + ) + } + } + return SyncRequest.Builder().DeviceAccountsPaylaodVALUE(builder.build()).build() + } + + private fun createDeviceInfoCollect( + context: Context, + gpuInfos: List + ): DeviceInfoCollect { + val builder = DeviceInfoCollect.Builder() + builder.reqTouchScreen(0).reqKeyboardType(0).reqNavigation(0).desityDeviceStablePoint(0) + .reqInputFeatures1(false) + .reqInputFeatures2(false).desityDeviceStable(0).reqGlEsVersion(0) + + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val configurationInfo = activityManager.deviceConfigurationInfo + if (configurationInfo != null) { + if (configurationInfo.reqTouchScreen != Configuration.TOUCHSCREEN_UNDEFINED) { + builder.reqTouchScreen(configurationInfo.reqTouchScreen) + } + if (configurationInfo.reqKeyboardType != Configuration.KEYBOARD_UNDEFINED) { + builder.reqKeyboardType(configurationInfo.reqKeyboardType) + } + if (configurationInfo.reqNavigation != Configuration.NAVIGATION_UNDEFINED) { + builder.reqNavigation(configurationInfo.reqNavigation) + } + builder.reqGlEsVersion(configurationInfo.reqGlEsVersion) + builder.reqInputFeatures1((configurationInfo.reqInputFeatures and 1) == 1) + .reqInputFeatures2( + (configurationInfo.reqInputFeatures and 2) > 0 + ) + } + + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val size = Point() + if (windowManager != null) { + val display = windowManager.defaultDisplay + display.getSize(size) + + builder.displaySizex(size.x).displaySizey(size.y) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.desityDeviceStable(DisplayMetrics.DENSITY_DEVICE_STABLE) + .desityDeviceStablePoint( + calculatePoint(size, DisplayMetrics.DENSITY_DEVICE_STABLE) + ) + } + + val configuration = context.resources.configuration + builder.screenLayout(configuration.screenLayout) + .smallestScreenWidthDp(configuration.smallestScreenWidthDp) + .systemSharedLibraryNames(Arrays.asList(*Objects.requireNonNull(context.packageManager.systemSharedLibraryNames))) + .locales(Arrays.asList(*context.assets.locales)) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.glExtensions( + gpuInfos.stream() + .flatMap { fetchedGlStrings: FetchedGlStrings -> Arrays.stream(fetchedGlStrings.glExtensions) } + .collect(Collectors.toList())) + .isLowRamDevice(activityManager.isLowRamDevice) + } + + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + builder.totalMem(memoryInfo.totalMem) + .availableProcessors(Runtime.getRuntime().availableProcessors()) + + val systemAvailableFeatures = context.packageManager.systemAvailableFeatures + for (featureInfo in systemAvailableFeatures) { + if (!TextUtils.isEmpty(featureInfo.name)) { + var featureInfoProto = FeatureInfoProto.Builder().build() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + featureInfoProto = FeatureInfoProto.Builder().name(featureInfo.name) + .version(featureInfo.version).build() + } + builder.featureInfos = builder.featureInfos.toMutableList().apply { + add(featureInfoProto) + } + builder.featureNames = builder.featureNames.toMutableList().apply { + add(featureInfoProto.name!!) + } + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + builder.supportedAbis(java.util.List.of(*Build.SUPPORTED_ABIS)) + } + + var prop = getSystemProperty("ro.oem.key1", "") + if (!TextUtils.isEmpty(prop)) { + builder.oemkey1(prop) + } + builder.buildCodeName(Build.VERSION.CODENAME) + prop = getSystemProperty("ro.build.version.preview_sdk_fingerprint", "") + if (!TextUtils.isEmpty(prop)) { + builder.previewSdkFingerprint(prop) + } + return builder.build() + } + + private fun createDeviceCapabilitiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val builder = DeviceCapabilitiesPayload.Builder() + builder.glExtensions(deviceInfoCollect.glExtensions) + for (featureInfoProto in deviceInfoCollect.featureInfos) { + builder.featureInfos = builder.featureInfos.toMutableList().apply { + add( + FeatureInfoProto.Builder().name(featureInfoProto.name) + .version(featureInfoProto.version).build() + ) + } + } + builder.systemSharedLibraryNames(deviceInfoCollect.systemSharedLibraryNames) + .locales(deviceInfoCollect.locales).unknowFlag(false) + return SyncRequest.Builder().DeviceCapabilitiesPayloadVALUE(builder.build()).build() + } + + private fun createDeviceInputPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val builder = DeviceInputPropertiesPayload.Builder() + builder.reqInputFeatures1(deviceInfoCollect.reqInputFeatures1) + .reqKeyboardType(deviceInfoCollect.reqKeyboardType) + .reqNavigation(deviceInfoCollect.reqNavigation) + return SyncRequest.Builder().DeviceInputPropertiesPayloadVALUE(builder.build()).build() + } + + private fun createDeviceModelPayloadRequest(): SyncRequest { + val builder = DeviceModelPayload.Builder() + builder.MANUFACTURER(Build.MANUFACTURER).MODEL(Build.MODEL).DEVICE(Build.DEVICE).PRODUCT( + Build.PRODUCT + ).BRAND(Build.BRAND) + return SyncRequest.Builder().DeviceModelPayloadVALUE(builder.build()).build() + } + + private fun createEnterprisePropertiesPayloadRequest(context: Context): SyncRequest { + val enterprisePropertiesPayload = EnterprisePropertiesPayload.Builder() + val devicePolicyManager = + context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val activeAdmins = devicePolicyManager.activeAdmins + if (activeAdmins != null) { + for (componentName in activeAdmins) { + val packageName = componentName.packageName + var packageInfo: PackageInfo? = null + try { + packageInfo = context.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNATURES + ) + } catch (ignored: Exception) { + } + + val isDeviceOwner = devicePolicyManager.isDeviceOwnerApp(packageName) + var isProfileOwner = false + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + isProfileOwner = devicePolicyManager.isProfileOwnerApp(packageName) + } + + val profileInfoTemp = + ProfileInfoTemp.Builder().packageName(componentName.packageName) + .policyTypeValue(if (isDeviceOwner) PolicyType.MANAGED_DEVICE else if (isProfileOwner) PolicyType.MANAGED_PROFILE else PolicyType.LEGACY_DEVICE_ADMIN) + .pkgSHA1(calculateSHA(packageInfo!!.signatures[0].toByteArray(), "SHA1")) + .pkgSHA256(calculateSHA(packageInfo.signatures[0].toByteArray(), "SHA256")) + .build() + val profileInfo = ProfileInfo.Builder().pkgName(profileInfoTemp.packageName) + .pkgSHA1(profileInfoTemp.pkgSHA1).pkgSHA256(profileInfoTemp.pkgSHA256) + .policyTypeValue(MangedScope.fromValue(profileInfoTemp.policyTypeValue!!.value)) + .build() + if (isProfileOwner) { + enterprisePropertiesPayload.profileOwner(profileInfo) + } + enterprisePropertiesPayload.mdefault = enterprisePropertiesPayload.mdefault.toMutableList().apply { + add(profileInfo) + } + } + } + return SyncRequest.Builder() + .enterprisePropertiesPayload(enterprisePropertiesPayload.build()).build() + } + + private fun createHardwareIdentifierPayloadRequest(context: Context): SyncRequest { + val builder = HardwareIdentifierPayload.Builder() + val telephonyManager = + context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + var imeid: Long = 0 + if (telephonyManager != null) { + //random imei + val randomIMEI = generateRandomIMEI() + imeid = if (TextUtils.isEmpty(randomIMEI) || !Pattern.compile("^[0-9]{15}$") + .matcher(randomIMEI).matches() + ) 0L else randomIMEI.toLong(10) or 0x1000000000000000L + if (imeid == 0L) { + var meid = "" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + meid = telephonyManager.meid + } + if (!TextUtils.isEmpty(meid) && Pattern.compile("^[0-9a-fA-F]{14}$").matcher(meid) + .matches() + ) { + imeid = meid.toLong(16) or 0x1100000000000000L + if (imeid == 0L) { + if (context.packageManager.checkPermission( + "android.permission.READ_PRIVILEGED_PHONE_STATE", + "com.android.vending" + ) == PackageManager.PERMISSION_GRANTED + ) { + var serial = "" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + serial = Build.getSerial() + } + if (TextUtils.isEmpty(serial) && serial != "unknown") { + try { + val serialShaByte = MessageDigest.getInstance("SHA1") + .digest(serial.toByteArray()) + imeid = + ((serialShaByte[0].toLong()) and 0xFFL) shl 0x30 or 0x1400000000000000L or (((serialShaByte[1].toLong()) and 0xFFL) shl 40) or (((serialShaByte[2].toLong()) and 0xFFL) shl 0x20) or (((serialShaByte[3].toLong()) and 0xFFL) shl 24) or (((serialShaByte[4].toLong()) and 0xFFL) shl 16) or (((serialShaByte[5].toLong()) and 0xFFL) shl 8) or ((serialShaByte[6].toLong()) and 0xFFL) + } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { + Log.w(TAG, "No support for sha1?") + } + } + } + } + } + } + builder.imeid(imeid) + } + return SyncRequest.Builder().HardwareIdentifierPayloadVALUE(builder.build()).build() + } + + private fun createHardwarePropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val HardwarePropertiesPayload_ = HardwarePropertiesPayload.Builder() + HardwarePropertiesPayload_.isLowRamDevice(deviceInfoCollect.isLowRamDevice) + .totalMem(deviceInfoCollect.totalMem) + .availableProcessors(deviceInfoCollect.availableProcessors) + .supportedAbis(deviceInfoCollect.supportedAbis).build() + return SyncRequest.Builder() + .HardwarePropertiesPayloadVALUE(HardwarePropertiesPayload_.build()).build() + } + + private fun createLocalePropertiesPayloadRequest(): SyncRequest { + val builder = LocalePropertiesPayload.Builder().b("GMT+08:00") + return SyncRequest.Builder().LocalePropertiesPayloadVALUE(builder.build()).build() + } + + private fun createPlayPartnerPropertiesPayloadRequest(): SyncRequest { + val builder = PlayPartnerPropertiesPayload.Builder() + builder.marketId("am-google").partnerIdMs("play-ms-android-google") + .partnerIdAd("play-ad-ms-android-google") + return SyncRequest.Builder().PlayPartnerPropertiesPayloadVALUE(builder.build()).build() + } + + private fun createPlayPropertiesPayload(context: Context): SyncRequest { + var version = 0 + try { + version = context.packageManager.getPackageInfo("com.android.vending", 0).versionCode + } catch (`packageManager$NameNotFoundException0`: PackageManager.NameNotFoundException) { + Log.w(TAG, "[DAS] Could not find our package", `packageManager$NameNotFoundException0`) + } + val playPropertiesPayload = PlayPropertiesPayload.Builder().playVersion(version).build() + return SyncRequest.Builder().PlayPropertiesPayloadVALUE(playPropertiesPayload).build() + } + + private fun createScreenPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val builder = ScreenPropertiesPayload.Builder() + builder.reqTouchScreen(deviceInfoCollect.reqTouchScreen) + .displaySizex(deviceInfoCollect.displaySizex) + .displaySizey(deviceInfoCollect.displaySizey) + .desityDeviceStablePoint(deviceInfoCollect.desityDeviceStablePoint) + .desityDeviceStable(deviceInfoCollect.desityDeviceStable) + return SyncRequest.Builder().ScreenPropertiesPayloadVALUE(builder.build()).build() + } + + private fun createSystemPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { + val SystemPropertiesPayload_ = SystemPropertiesPayload.Builder() + SystemPropertiesPayload_.fingerprint("google/sunfish/sunfish:13/TQ2A.230405.003/9719927:user/release-keys") + .sdkInt(Build.VERSION.SDK_INT.toLong()) + .previewSdkFingerprint(deviceInfoCollect.previewSdkFingerprint) + .buildCodeName(deviceInfoCollect.buildCodeName).oemkey1(deviceInfoCollect.oemkey1) + .reqGlEsVersion(deviceInfoCollect.reqGlEsVersion) + return SyncRequest.Builder().SystemPropertiesPayloadVALUE(SystemPropertiesPayload_.build()) + .build() + } + + private fun createGpuPayloadRequest(gpuInfos: List): SyncRequest { + var gpuInfos = gpuInfos + var gpuPayloads = emptyList() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + gpuInfos = gpuInfos.stream() + .filter { fetchedGlStrings: FetchedGlStrings -> !fetchedGlStrings.glRenderer!!.isEmpty() || !fetchedGlStrings.glVendor!!.isEmpty() || !fetchedGlStrings.glVersion!!.isEmpty() } + .collect(Collectors.toList()) + val maxVersion = gpuInfos.stream() + .max(Comparator.comparingInt { fetchedGlStrings: FetchedGlStrings -> fetchedGlStrings.contextClientVersion }) + .map { obj: FetchedGlStrings -> obj.contextClientVersion } + if (maxVersion.isPresent) { + gpuInfos = gpuInfos.stream() + .filter { fetchedGlStrings: FetchedGlStrings -> fetchedGlStrings.contextClientVersion == maxVersion.get() } + .collect(Collectors.toList()) + } + gpuPayloads = gpuInfos.stream().map { fetchedGlStrings: FetchedGlStrings -> + val gpuInfoWrapper_ = GpuInfoWrapper.Builder() + if (!TextUtils.isEmpty(fetchedGlStrings.glRenderer)) gpuInfoWrapper_.glRenderer( + fetchedGlStrings.glRenderer + ) + if (!TextUtils.isEmpty(fetchedGlStrings.glVendor)) gpuInfoWrapper_.glVendor( + fetchedGlStrings.glVendor + ) + if (!TextUtils.isEmpty(fetchedGlStrings.glVersion)) gpuInfoWrapper_.glVersion( + fetchedGlStrings.glVersion + ) + GpuPayload.Builder().gpuInfo(gpuInfoWrapper_.build()).build() + }.distinct().collect(Collectors.toList()) + } + + return SyncRequest.Builder().GpuPayloadVALUE( + if (gpuPayloads.isEmpty()) GpuPayload.Builder().build() else gpuPayloads[0] + ).build() + } + + private fun fetchGLInfo(): ArrayList? { + Log.d(TAG, "fetchGLInfo: ") + val eGL100 = EGLContext.getEGL() as EGL10 + val result = ArrayList() + val egl10Instance = if (eGL100 == null) null else EGL10Wrapper(eGL100) + if (eGL100 == null) { + Log.w(TAG, "Couldn't get EGL") + return null + } + val eglDisplay = eGL100.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) + eGL100.eglInitialize(eglDisplay, IntArray(2)) + val numConfig = IntArray(1) + val configCount = + if (eGL100.eglGetConfigs(eglDisplay, null, 0, numConfig)) numConfig[0] else 0 + if (configCount <= 0) { + Log.w(TAG, "Couldn't get EGL config count") + return null + } + var configs: Array? = arrayOfNulls(configCount) + configs = if (eGL100.eglGetConfigs( + eglDisplay, + configs, + configCount, + IntArray(1) + ) + ) configs else null + if (configs == null) { + Log.w(TAG, "Couldn't get EGL configs") + return null + } + val arr_v1 = intArrayOf( + EGL10.EGL_WIDTH, + EGL10.EGL_PBUFFER_BIT, + EGL10.EGL_HEIGHT, + EGL10.EGL_PBUFFER_BIT, + EGL10.EGL_NONE + ) + for (index in 0 until configCount) { + if (egl10Instance!!.eglGetConfigAttrib( + eglDisplay, + configs[index], + EGL10.EGL_CONFIG_CAVEAT + ) != 0x3050 + && (egl10Instance.eglGetConfigAttrib( + eglDisplay, + configs[index], + EGL10.EGL_SURFACE_TYPE + ) and 1) != 0 + ) { + val attributeValue = egl10Instance.eglGetConfigAttrib( + eglDisplay, + configs[index], + EGL10.EGL_RENDERABLE_TYPE + ) + if ((attributeValue and 1) != 0) { + result.add( + buildGLStrings( + egl10Instance, + eglDisplay, + configs[index], + arr_v1, + null + ) + ) + } + + if ((attributeValue and 4) != 0) { + result.add( + buildGLStrings( + egl10Instance, + eglDisplay, + configs[index], + arr_v1, + intArrayOf(0x3098, EGL10.EGL_PIXMAP_BIT, EGL10.EGL_NONE) + ) + ) + } + } + } + egl10Instance!!.eglinstance.eglTerminate(eglDisplay) + return result + } + + private fun buildGLStrings( + egl10Tools: EGL10Wrapper?, + eglDisplay: EGLDisplay, + eglConfig: EGLConfig?, + arr_v: IntArray, + arr_v1: IntArray? + ): FetchedGlStrings? { + val eglContext = egl10Tools!!.eglinstance.eglCreateContext( + eglDisplay, + eglConfig, + EGL10.EGL_NO_CONTEXT, + arr_v1 + ) + if (eglContext !== EGL10.EGL_NO_CONTEXT) { + val eglSurface = + egl10Tools.eglinstance.eglCreatePbufferSurface(eglDisplay, eglConfig, arr_v) + if (eglSurface === EGL10.EGL_NO_SURFACE) { + egl10Tools.eglDestroyContext(eglDisplay, eglContext) + return null + } + egl10Tools.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) + val result = FetchedGlStrings(0, null, null, null, null) + val glExtensions = GLES10.glGetString(GLES10.GL_EXTENSIONS) + if (!TextUtils.isEmpty(glExtensions)) { + result.glExtensions = + glExtensions.split(" ".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + } + result.glRenderer = GLES10.glGetString(GLES10.GL_RENDERER) + result.glVendor = GLES10.glGetString(GLES10.GL_VENDOR) + result.glVersion = GLES10.glGetString(GLES10.GL_VERSION) + if (result.glExtensions != null) { + egl10Tools.eglMakeCurrent( + eglDisplay, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT + ) + egl10Tools.eglinstance.eglDestroySurface(eglDisplay, eglSurface) + egl10Tools.eglDestroyContext(eglDisplay, eglContext) + return result + } + + val stringBuilder = StringBuilder() + + if (result.glExtensions == null) { + stringBuilder.append(" glExtensions") + } + throw IllegalStateException("Missing required properties:$stringBuilder") + } + return null + } + + fun calculateSHA(data: ByteArray, algorithm: String?): String? { + val messageDigest0: MessageDigest + try { + messageDigest0 = MessageDigest.getInstance(algorithm) + } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { + Log.w(TAG, "[DC] No support for %s?", noSuchAlgorithmException0) + return null + } + + messageDigest0.update(data, 0, data.size) + return Base64.encodeToString(messageDigest0.digest(), 11) + } + + fun getSystemProperty(key: String?, defaultValue: String?): String? { + var value = defaultValue + try { + @SuppressLint("PrivateApi") val systemPropertiesClass = + Class.forName("android.os.SystemProperties") + val getMethod = + systemPropertiesClass.getMethod("get", String::class.java, String::class.java) + value = getMethod.invoke(null, key, defaultValue) as String + } catch (e: Exception) { + Log.w(TAG, "Unable to retrieve system property", e) + } + return value + } + + fun calculatePoint(point: Point, v: Int): Int { + val f = point.x.toFloat() + val v1 = ((point.y.toFloat()) * (160.0f / (v.toFloat()))).toInt() + if (v1 < 470) { + return 17 + } + + val v2 = (f * (160.0f / (v.toFloat()))).toInt() + if (v1 >= 960 && v2 >= 720) { + return if (v1 * 3 / 5 < v2 - 1) 20 else 4 + } + + val v3 = if (v1 < 640 || v2 < 480) 2 else 3 + return if (v1 * 3 / 5 < v2 - 1) v3 or 16 else v3 + } + + class EGL10Wrapper internal constructor(val eglinstance: EGL10) { + fun eglGetConfigAttrib(eglDisplay: EGLDisplay?, eglConfig: EGLConfig?, v: Int): Int { + val value = IntArray(1) + eglinstance.eglGetConfigAttrib(eglDisplay, eglConfig, v, value) + eglinstance.eglTerminate(eglDisplay) + return value[0] + } + + fun eglDestroyContext(eglDisplay: EGLDisplay?, eglContext: EGLContext?) { + eglinstance.eglDestroyContext(eglDisplay, eglContext) + } + + fun eglMakeCurrent( + eglDisplay: EGLDisplay?, + draw: EGLSurface?, + read: EGLSurface?, + eglContext: EGLContext? + ) { + eglinstance.eglMakeCurrent(eglDisplay, draw, read, eglContext) + } + } + + + class FetchedGlStrings( + var contextClientVersion: Int, + var glExtensions: Array?, + var glRenderer: String?, + var glVendor: String?, + var glVersion: String? + ) +} 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 new file mode 100644 index 0000000000..c7b194bcd6 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/phonesky/header/PhoneskyHeaderValue.kt @@ -0,0 +1,406 @@ +package com.google.android.phonesky.header + +import android.accounts.Account +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.text.TextUtils +import android.util.Base64 +import android.util.Log +import androidx.collection.arrayMapOf +import com.android.vending.ExperimentAndConfigs.readExperimentsFlag +import com.android.vending.ExperimentAndConfigs.toByteArray +import com.google.android.phonesky.header.PayloadsProtoStore.readCache +import okio.ByteString.Companion.encodeUtf8 +import org.microg.vending.billing.CheckinServiceClient.getConsistencyToken +import org.microg.vending.billing.GServices.getString +import java.io.ByteArrayOutputStream +import java.io.DataOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.OutputStream +import java.net.HttpURLConnection +import java.net.URL +import java.util.zip.GZIPOutputStream + +object PhoneskyHeaderValue { + var TAG: String = PhoneskyHeaderValue::class.java.simpleName + private const val PHONESKY_HEADER_FILE = "finsky/shared/phonesky_header_valuestore.pb" + + @SuppressLint("HardwareIds") + fun init(applicationContext: Context): PhoneskyValueStore? { + try { + val initiated = PhoneskyValue.Builder() + + initiated.ConsistencyTokenWrapperValue( + ConsistencyTokenWrapper.Builder() + .ConsistencyToken(getConsistencyToken(applicationContext)) + .unknowTokenf("").build() + ) + + initiated.baseDeviceInfoValue( + BaseDeviceInfo.Builder() + .device(Build.DEVICE) + .hardware(Build.HARDWARE) + .model(Build.MODEL) + .product(Build.PRODUCT) + .androidId( + getString(applicationContext.contentResolver, "android_id", "")!! + .toLong() + ) + .gpVersion( + Uri.encode( + applicationContext.packageManager.getApplicationInfo( + applicationContext.packageName, + PackageManager.GET_META_DATA + ).metaData.getString("GpVersion") + ).replace("(", "%28").replace(")", "%29") + ) + .fingerPrint(Build.FINGERPRINT).build() + ) + + initiated.deviceBuildInfoValue( + DeviceBuildInfo.Builder() + .buildInfo( + BuildInfo.Builder() + .sdkInt(Build.VERSION.SDK_INT) + .id(Build.ID) + .release(Build.VERSION.RELEASE) + .constInte(84122130).build() + ) + .marketClientId("am-google") //"market_client_id" + .unknowBooleD(true) //getResources(xxx)^1 + .build() + ) + + val result = PhoneskyValueStore.Builder() + val phoneskyValueMutableMap = result.values.toMutableMap() + phoneskyValueMutableMap[""] = initiated.build() + result.values = phoneskyValueMutableMap + return result.build() + } catch (e: Exception) { + Log.w(TAG, "PhoneskyHeaderValue.Init", e) + } + return null + } + + fun getPhoneskyHeader(context: Context, account: Account) { + var request = GoogleApiRequest( + "https://play-fe.googleapis.com/fdfe/toc?nocache_isui=true", + "GET", + account, + context, + buildataFdfe(context) + ) + val result = request.sendRequest(null) + val tocToken = TocToken.Builder() + .token(result!!.fdfeApiResponseValue?.tocApi?.tocTokenValue?.encodeUtf8()) + .build() + writePhonesky(context, "", object : WritePhoneskyCallback { + override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { + return data.tocTokenValue(tocToken).build() + } + + }) + + val firstSyncData = SyncReqWrapper.Builder().mvalue( + listOf( + SyncRequest.Builder() + .UnknowTypeFirstSyncValue(UnknowTypeFirstSync.Builder().build()).build() + ) + ).build() + request = GoogleApiRequest( + "https://play-fe.googleapis.com/fdfe/sync", + "POST", + account, + context, + buildataFdfe(context) + ) + request.content = firstSyncData.encode() + val resultSyncFirst = request.sendRequest(null) + + writePhonesky(context, "", object : WritePhoneskyCallback { + override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { + return data.sysncTokenValue( + resultSyncFirst!!.fdfeApiResponseValue?.syncResult?.syncTokenValue + ).build() + } + }) + + val requestData = readCache(context) + request = GoogleApiRequest( + "https://play-fe.googleapis.com/fdfe/sync?nocache_qos=lt", + "POST", + account, + context, + buildataFdfe(context) + ) + request.content = requestData!!.encode() + val resultSync = request.sendRequest(null) + + writePhonesky(context, "", object : WritePhoneskyCallback { + override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { + return data.sysncTokenValue( + resultSync!!.fdfeApiResponseValue?.syncResult?.syncTokenValue + ).build() + } + }) + + writePhonesky(context, account.name, object : WritePhoneskyCallback { + override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { + return data.experimentWrapperValue( + ExperimentWrapper.Builder().experServerTokenValue( + getExperimentTokenFor(context, account) + ).build() + ).build() + } + }) + + writePhonesky(context, "", object : WritePhoneskyCallback { + override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { + return data.experimentWrapperValue( + ExperimentWrapper.Builder().experServerTokenValue( + getExperimentTokenFor(context, null) + ).build() + ).build() + } + }) + } + + private fun getExperimentTokenFor(context: Context, account: Account?): ExperServerToken { + val dataRegular = readExperimentsFlag( + context, + "com.google.android.finsky.regular", + if (account == null) "" else account.name + ) + val dataStable = readExperimentsFlag(context, "com.google.android.finsky.stable", "") + val result = ExperServerToken.Builder() + if (dataRegular != null && !TextUtils.isEmpty(dataRegular.serverToken)) { + result.regularServerToken(dataRegular.serverToken) + } + if (dataStable != null && !TextUtils.isEmpty(dataStable.serverToken)) { + result.stableServerToken(dataStable.serverToken) + } + return result.build() + } + + //build base X-PS-RH for /fdfe/* + fun buildataFdfe(context: Context): PhoneskyValue { + return PhoneskyValue.Builder() + .unknowFieldk(PhoneskyUnknowFieldK.Builder().mvalue(5).build()) + .baseDeviceInfoValue( + BaseDeviceInfo.Builder().androidId( + getString(context.contentResolver, "android_id", "")!! + .toLong() + ).build() + ) + .unknowDeviceIdValue( + UnknowDeviceId.Builder().uuid("00000000-0000-0000-0000-000000000000").type(1) + .build() + ).build() + } + + private fun writePhonesky(context: Context, key: String, callback: WritePhoneskyCallback) { + val file = File(context.filesDir, PHONESKY_HEADER_FILE) + var existData: PhoneskyValueStore.Builder? = null + if (file.exists()) { + val input = FileInputStream(file) + existData = PhoneskyValueStore.ADAPTER.decode(input).newBuilder() + input.close() + } else { + if (file.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) { + if (file.createNewFile()) { + existData = init(context)?.newBuilder() + } else { + throw RuntimeException("create file failed") + } + } + } + if (existData != null) { + val phoneskyValueMap = existData.values.toMutableMap() + if (existData.values.containsKey(key)) { + + val modifed = callback.modify(if (existData.values[key] != null) { + existData.values[key]!!.newBuilder() + } else { + PhoneskyValue.Builder() + }) + phoneskyValueMap[key] = modifed + } else { + val modifed = callback.modify(PhoneskyValue.Builder()) + phoneskyValueMap[key] = modifed + } + existData.values = phoneskyValueMap + val outputStream = FileOutputStream(file) + outputStream.write(existData.build().encode()) + outputStream.close() + } + } + + interface WritePhoneskyCallback { + fun modify(data: PhoneskyValue.Builder): PhoneskyValue + } + + class GoogleApiRequest( + var url: String, + var method: String, + private val user: Account, + var context: Context, + private val externalxpsrh: PhoneskyValue? + ) { + var content: ByteArray? = null + var timeout: Int = 3000 + var headerMap: MutableMap = arrayMapOf() + private val tokenType = "oauth2:https://www.googleapis.com/auth/googleplay" + var gzip: Boolean = false + + init { + headerMap["User-Agent"] = buildUserAgent() + } + + @SuppressLint("DefaultLocale") + private fun buildUserAgent(): String { + val versionName = "41.2.21-31" + val versionCode = "84122130" + val apiLevel = Build.VERSION.SDK_INT + val device = Build.DEVICE + val hardware = Build.HARDWARE + val product = Build.PRODUCT + val release = Build.VERSION.RELEASE + val model = Build.MODEL + val buildId = Build.ID + var supportedAbis: String? = null + supportedAbis = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + java.lang.String.join(";", *Build.SUPPORTED_ABIS) + } else { + Build.CPU_ABI + ";" + Build.CPU_ABI2 + } + + return String.format( + "Android-Finsky/%s [0] [PR] 636997666 (api=%d,versionCode=%s,sdk=%d,device=%s,hardware=%s,product=%s,platformVersionRelease=%s,model=%s,buildId=%s,isWideScreen=%d,supportedAbis=%s)", + versionName,apiLevel,versionCode,apiLevel, + device, + hardware, + product, + release, + model, + buildId, + 0, + supportedAbis + ) + } + + fun addHeader(key: String, value: String) { + headerMap[key] = value + } + + fun getHeaders(): Map { + val phoneksyHeaderFile = File(context.filesDir, PHONESKY_HEADER_FILE) + var existData = PhoneskyValueStore.Builder().build() + if (phoneksyHeaderFile.exists()) { + val input = FileInputStream(phoneksyHeaderFile) + existData = PhoneskyValueStore.ADAPTER.decode(input) + input.close() + } + var xpsrh = PhoneskyValue.Builder().build() + if (existData.values.containsKey("")) { + xpsrh = existData.values[""]!! + } + if (existData.values.containsKey(user.name)) { + mergeProto(xpsrh, existData.values[user.name]) + } + if (externalxpsrh != null) { + mergeProto(xpsrh, externalxpsrh) + } + headerMap["X-PS-RH"] = Base64.encodeToString( + gzip( + xpsrh!!.encode() + ), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + ) + headerMap["Authorization"] = "Bearer " + AccountManager.get(context).getAuthToken( + user, tokenType, null, false, null, null + ).result.getString(AccountManager.KEY_AUTHTOKEN) + return this.headerMap + } + + fun mergeProto(data1: PhoneskyValue?, data2: PhoneskyValue?) { + for (data in PhoneskyValue::class.java.declaredFields) { + data.isAccessible = true + if (data[data2] != null && data[data1] == null) { + data[data1] = data[data2] + } + } + } + + 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 = toByteArray(httpURLConnection.inputStream) + return GoogleApiResponse.ADAPTER.decode(data) + } + + return null + } + + companion object { + fun gzip(arr_b: ByteArray?): ByteArray { + try { + ByteArrayOutputStream().use { byteArrayOutputStream -> + GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> + gzipOutputStream.write(arr_b) + gzipOutputStream.finish() + val arr_b1 = byteArrayOutputStream.toByteArray() + arr_b1[9] = 0 + return arr_b1 + } + } + } catch (iOException0: IOException) { + Log.w("Unexpected %s", arrayOf(iOException0).contentToString()) + return ByteArray(0) + } + } + } + } +} diff --git a/vending-app/src/main/proto/ExperimentsAndConfigs.proto b/vending-app/src/main/proto/ExperimentsAndConfigs.proto new file mode 100644 index 0000000000..142e60857b --- /dev/null +++ b/vending-app/src/main/proto/ExperimentsAndConfigs.proto @@ -0,0 +1,167 @@ + +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +message experimentRequestData { + optional DeviceData deviceDataValue = 1; + repeated ExperimentsInfo experimentsInfo = 2; + optional bytes bytesTag = 3; + optional action actionType = 4; + optional int32 unknowFieldG = 5; + optional string expPkgName = 7; +} + +enum action { + UNSPECIFIED =0; + PERIODIC = 1; + ADAPTIVE = 2; + GCM_PUSH = 3; + APPLICATION_PUSH= 4; + APPLICATION_PUSH_RETRY=11; + NEW_USER= 5; + NEW_APPLICATION= 6; + NEW_REGISTER_VERSION= 7; + NEW_REGISTER_OTHER= 8; + MOBDOG= 9; + RETRY_AFTER= 10; + NEW_USER_SYNC= 12; + NEW_APPLICATION_SYNC= 13; + NEW_REGISTER_VERSION_SYNC= 14; + NEW_REGISTER_OTHER_SYNC= 15; + NEW_APP_PROPERTIES= 16; + GMSCORE_SAFEBOOT= 17; + CHECK_IN= 18; + FORCE_SYNC= 19; + FLAG_CORRUPTION= 20; + FLAG_OVERRIDE= 21; + MOBILE_UTILITIES= 22; +} + +message ExperimentsInfo { + optional ExperimentVersion experimentVersionValue = 1; + optional bytes unKnowBytesC = 2; + repeated ApplicationTag applicationTagValue =3; + optional bytes tokensTag = 4; +} + +message UnknowMsg { + optional int32 field1 = 1; +} + +message ApplicationTag { + optional int64 partitionId = 1; + optional bytes tag = 2; +} + +message ExperimentVersion { + optional string expPkgName = 1; + optional int64 version = 2; + optional ExperimentFlag experimentFlagValue = 3; + optional sfixed64 baselineCL = 4; + optional string pkgName = 5; +} + +message ExperimentFlag { + optional fixed64 flag = 1; +} + +message DeviceData { + optional int64 hasAccount = 2; + optional ExpDeviceInfoWrapper expDeviceInfoWrapperValue = 4; + optional bytes unknowEmptyE = 6; + optional bool unknowFlagf = 7; + optional DeviceDataEmptyA unknkowFieldG = 8; +} + +message DeviceDataEmptyA { + optional bytes unknowFieldB = 1; +} + +message ExpDeviceInfoWrapper{ + optional int32 unknowFieldb = 1; + optional ExpDeviceInfo expDeviceInfoValue = 2; +} + + +message ExpDeviceInfo { + optional int64 androidId = 1; + optional int32 sdkInt = 3; + optional string model = 4; + optional string product = 5; + optional string buildId = 6; + optional string buildDevice = 9; + optional string unknowEmpty = 10; + optional string locale = 11; + optional string country = 12; + optional string manufacturer = 13; + optional string fingerprint = 17; + repeated string supportAbis = 31; +} + + +message ExperimentResponseData { + repeated ExperimentsDataWrapper experiments =1; + optional bytes bytesTag = 2; + optional int64 servingVersion = 4; +} + +message ExperimentsDataWrapper { + optional ExperimentVersion experimentVersionValue = 1; + repeated ExpFlagsGroup expFlagsGroupValue =2; //c + optional bytes experimentToken = 3; + optional string serverToken = 4; + optional bytes tokensTag = 7; +} + +//message autv { +// optional experimentVersion experimentVersionValue = 1; +//} + +message ExpFlagsGroup { + optional ApplicationTag applicationTagValue = 1; + repeated ExpFlag expFlags =2; +} + +message ExpFlag { + optional string flagName = 1; + optional int64 longValue = 2; + optional bool boolValue = 3; + optional double doubleValue = 4; + optional string stringValue = 5; + optional ExtensionValue extensionValueValue = 6; + optional int32 valueType = 9; //enum +} + +message ExtensionValue { + optional bytes mvalue = 1; +} + +//----------------------------experimentsFlags protostore message---- +message ExperimentsFlagsProto { + optional string expeIntroduce = 2; //b + optional bytes experimentToken = 3; //c + optional string serverToken = 1; //d + optional bool unknowFlagB = 8; //g + optional int64 servingVersion = 9; //h + repeated FlagValueProto flagValues =4; //e +} + +message FlagValueProto { + oneof c { + fixed64 intVal = 1; + bool boolVal = 2; + double floatVal = 3; + string stringVal = 4; + bytes extensionVal = 5; + } + optional string name = 10; +} + +message ExperimentTokenStore { + repeated bytes experimentToken = 2; //c +} + + + + + diff --git a/vending-app/src/main/proto/PhoneskyHeaderValueStore.proto b/vending-app/src/main/proto/PhoneskyHeaderValueStore.proto new file mode 100644 index 0000000000..62fbc3fefd --- /dev/null +++ b/vending-app/src/main/proto/PhoneskyHeaderValueStore.proto @@ -0,0 +1,433 @@ + +option java_package = "com.google.android.phonesky.header"; +option java_multiple_files = true; + +message PhoneskyValueStore{//aknj + map values = 2; +} + +message PhoneskyValue{ //ayie + optional SyncToken sysncTokenValue = 1; + optional ExperimentWrapper experimentWrapperValue = 10; + optional TocToken tocTokenValue = 11; //j + optional PhoneskyUnknowFieldK unknowFieldk = 12; + optional RequestLanguagePkg languages = 15; + optional ConsistencyTokenWrapper ConsistencyTokenWrapperValue = 16; //n + optional DeviceBuildInfo deviceBuildInfoValue = 20; //q + optional BaseDeviceInfo baseDeviceInfoValue = 21; //r + optional UnknowDeviceId unknowDeviceIdValue = 27; +} + +message UnknowDeviceId { + optional string uuid = 1; + optional int32 type = 2; //enum +} + +message BaseDeviceInfo { + optional string device = 1; + optional string hardware = 2; + optional string model = 3; + optional string gpVersion = 4; //e + optional string product = 5; + optional int64 androidId = 6; //f + optional string fingerPrint = 7; +} + +message DeviceBuildInfo { + optional BuildInfo buildInfo = 1; + optional string marketClientId = 2; //c + optional bool unknowBooleD = 3; //d +} + +message BuildInfo { + optional int32 sdkInt = 1; //b + optional string id = 2; //c + optional string release = 3; //d + optional int32 constInte = 4; //e +} + +message ConsistencyTokenWrapper { + optional string ConsistencyToken = 1; //b + optional string unknowTokenc = 2; //c + optional string unknowTokenf = 6; //f +} + +message RequestLanguagePkg { + repeated string language = 1; +} + +message PhoneskyUnknowFieldK { + optional int32 mvalue = 1; //enum +} + +message TocToken { + optional bytes token = 1; +} + +message ExperimentWrapper { + optional ExperServerToken experServerTokenValue = 1; +} + +message ExperServerToken { + optional string regularServerToken = 1; + optional string stableServerToken = 2; + optional bytes unknowField = 3; +} + +message SyncToken { + optional string mvalue = 1; +} + +enum PaurchaseType { + UNKNOWN_ITEM_TYPE = 0; + ANDROID_APP = 1; + ANDROID_APP_DEVELOPER = 2; + ANDROID_IN_APP_ITEM = 3; + DYNAMIC_ANDROID_IN_APP_ITEM = 4; + ANDROID_APP_SUBSCRIPTION = 5; + DYNAMIC_ANDROID_APP_SUBSCRIPTION = 6; + MOVIE = 7; + TV_SHOW = 8; + TV_SEASON = 9; + TV_EPISODE =10; + AUDIOBOOK =11; + AUDIOBOOK_SERIES =24; + EBOOK =12; + EBOOK_SERIES =13; + BOOK_AUTHOR =14; + ALBUM =15; + SONG =16; + MUSIC_ARTIST =17; + MAGAZINE =18; + MAGAZINE_ISSUE =19; + NEWSPAPER =20; + NEWS_ISSUE =21; + VOUCHER =22; + YOUTUBE_COMMERCE_ITEM =25; + LOYALTY_REWARD =26; + BOOK_SUBSCRIPTION =27; + LOYALTY_VOUCHER =28; + LOYALTY_PLAY_CREDIT =29; +} + +enum DeviceType { + NO_DEVICE = 0; + PHONE =1 ; + GTV =2 ; + TABLET =3 ; + TABLET_LARGE =4 ; + ANDROID_TV =5 ; + WEAR =6 ; + CHROMEBOOK =7 ; + ANDROID_AUTO =8 ; + HIGH_PERFORMANCE_EMULATOR =10 ; + ANDROID_XR =11 ; +} +//message awgc { +// optional feauture b = 1; +//} + +enum Feauture { + FEATURED_UNSPECIFIED =0; + FEATURED_TV_MOVIES =1; + FEATURED_ENTERTAINMENT_VIDEO =2; + FEATURED_EBOOK =3; + FEATURED_AUDIOBOOK =4; + FEATURED_BOOK_SERIES =5; + FEATURED_MUSIC =6; + FEATURED_PODCAST =7; + FEATURED_RADIO =8; + FEATURED_SHOPPING_PRODUCT =9; + FEATURED_FOOD_PRODUCT =10; + FEATURED_RECIPE =11; + FEATURED_FOOD_STORE =12; + FEATURED_GENERIC_CONTENT =13; +} + +message GoogleApiResponse { + optional FdfeApiResponse fdfeApiResponseValue = 1; + optional UnknowTypebbfe g= 5; + optional bytes unknowFieldBytes= 9; +} + +message UnknowTypebbfe { + optional int64 id=1; +} + +message FdfeApiResponse { + optional TocResponse tocApi = 6; + optional SplitResponse splitReqResult = 21; + optional SyncApiResp syncResult = 183; +} + +message TocResponse { +// optional bool o=11; + optional string tocTokenValue=22; //t +} + +message SplitResponse { + optional int32 b = 1; //unknow enum + optional PkgFetchInfo pkgList = 2; +} + +message PkgFetchInfo { + repeated SplitPkgInfo pkgDownlaodInfo = 15; +} + +message SplitPkgInfo { + optional string splitPkgName = 1; + optional int64 size = 2; + optional string checkSum = 4; + optional string downloadUrl1 = 5; + optional DownloadInfo slaveDownloadInfo = 8; + optional string mabyChecksum = 9; + optional string unknowPkgInfoF = 15; + optional DownloadInfo otherDownloadInfo = 16; +} + +message DownloadInfo { + optional int32 id = 1; //unknow enum + optional int64 size = 2; + optional string url = 3; +} + +//--------------------------request for /fdfe/sync-- + +message SyncReqWrapper { + repeated SyncRequest mvalue = 1; +} + +message SyncRequest { + oneof payload { + AccountAossiationPayload AccountAossiationPayloadVALUE = 7; + DeviceAccountsPaylaod DeviceAccountsPaylaodVALUE = 8; + CarrierPropertiesPayload CarrierPropertiesPayloadVALUE = 9; + DeviceCapabilitiesPayload DeviceCapabilitiesPayloadVALUE = 10; + DeviceInputPropertiesPayload DeviceInputPropertiesPayloadVALUE = 11; + DeviceModelPayload DeviceModelPayloadVALUE = 12; + EnterprisePropertiesPayload enterprisePropertiesPayload = 13; + HardwareIdentifierPayload HardwareIdentifierPayloadVALUE = 14; + HardwarePropertiesPayload HardwarePropertiesPayloadVALUE = 15; + LocalePropertiesPayload LocalePropertiesPayloadVALUE = 16; + NotificationRoutingInfoPayload NotificationRoutingInfoPayloadVALUE = 17; + PlayPartnerPropertiesPayload PlayPartnerPropertiesPayloadVALUE = 18; + PlayPropertiesPayload PlayPropertiesPayloadVALUE = 19; + ScreenPropertiesPayload ScreenPropertiesPayloadVALUE = 20; + SystemPropertiesPayload SystemPropertiesPayloadVALUE = 21; + GpuPayload GpuPayloadVALUE = 24; + UnknowTypeFirstSync UnknowTypeFirstSyncValue = 30; + } +} + +message AccountAossiationPayload { + optional AccountAssValue mvalue = 1; +} + +message AccountAssValue { + optional string mvalue = 1; +} + +message DeviceAccountsPaylaod { + repeated AccountAssValue mvalue = 1; +} + +message CarrierPropertiesPayload { + optional string simOperator = 1; + optional TelephonyStateWrapper telephonyStateValue = 2; +} + +message TelephonyStateWrapper { + optional TelephonyInfo mvalue = 1; +} + +message TelephonyInfo { + optional int64 subscriberId1 = 1; + optional string operatorName = 2; + optional string groupidLevel = 3; +// optional string e = 4; +// repeated string f = 5; + optional int32 simcardId = 6; + optional int32 CarrierIdFromSimMccMnc = 7; +} + +message DeviceCapabilitiesPayload { + repeated FeatureInfoProto featureInfos = 1; + repeated string systemSharedLibraryNames = 2; + repeated string locales = 3; + repeated string glExtensions = 4; + optional bool unknowFlag = 5; +} + +message DeviceInputPropertiesPayload { + optional int32 reqKeyboardType = 1; //unknow enum + optional bool reqInputFeatures1 = 2; + optional int32 reqNavigation = 3; //unknow enum +} + +message DeviceModelPayload { + optional string MANUFACTURER = 1; + optional string MODEL = 2; + optional string DEVICE = 3; + optional string PRODUCT = 4; + optional string BRAND = 5; +} + +message EnterprisePropertiesPayload { + optional ProfileInfo profileOwner = 1; + repeated ProfileInfo mdefault = 2; +} + +message ProfileInfo { + optional string pkgName = 1; + optional string pkgSHA1 = 2; + optional string pkgSHA256 = 3; + optional MangedScope policyTypeValue = 4; //unknow enum +} + +enum MangedScope { + UNKNOWN_MANAGED_SCOPE = 0; + MANAGED_DEVICES = 1; + MANAGED_PROFILES = 2; + MANAGED_AVENGER = 3; + LEGACY_DEVICE_ADMINS = 4; +} + +message HardwareIdentifierPayload { + optional fixed64 imeid = 1; +} + +message HardwarePropertiesPayload { + optional bool isLowRamDevice = 1; + optional int64 totalMem = 2; + optional int32 availableProcessors = 3; + repeated string supportedAbis = 4; +} + +message LocalePropertiesPayload { + optional string b = 1; +} + +message NotificationRoutingInfoPayload { + optional string locale = 1; +} + +message PlayPartnerPropertiesPayload { + optional string marketId = 1; + optional string partnerIdMs = 2; + optional string partnerIdAd = 3; +} + +message PlayPropertiesPayload { + optional int32 playVersion = 2; +} + +message ScreenPropertiesPayload { + optional int32 reqTouchScreen = 1; //unknow enum + optional int32 displaySizex = 2; + optional int32 displaySizey = 3; + optional int32 desityDeviceStablePoint = 4; //unknow enum + optional int32 desityDeviceStable = 5; +} + +message SystemPropertiesPayload { + optional string fingerprint = 1; + optional int64 sdkInt = 2; + optional string previewSdkFingerprint = 3; + optional string buildCodeName = 4; + optional string oemkey1 = 5; + optional int32 reqGlEsVersion = 6; +} + +message GpuPayload { + optional GpuInfoWrapper gpuInfo = 1; +} + +message GpuInfoWrapper { + optional string glRenderer = 1; + optional string glVendor = 2; + optional string glVersion = 3; +} + + + +message UnknowTypeFirstSync { +} + +//-----------------response for fdfe/sync +message SyncApiResp { + repeated SyncApiRespEmptyA unknowFieldA=1; + optional SyncToken syncTokenValue=2; +// repeated string c=3; +} + +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; +} + +//----------------------proto for payloads protostroe------ + +message DeviceInfoCollect { + optional int32 reqTouchScreen = 1; //unknow enum + optional int32 reqKeyboardType = 2; //unknow enum + optional int32 reqNavigation = 3; //unknow enum + optional int32 desityDeviceStablePoint = 4; //unknow enum + optional bool reqInputFeatures1 = 5; + optional bool reqInputFeatures2 = 6; + optional int32 desityDeviceStable = 7; //DENSITY_DEVICE_STABLE + optional int32 reqGlEsVersion = 8; + repeated string systemSharedLibraryNames = 9; + repeated string featureNames = 10; + repeated string supportedAbis = 11; + optional int32 displaySizex = 12; + optional int32 displaySizey = 13; + repeated string locales = 14; + repeated string glExtensions = 15; + optional int32 smallestScreenWidthDp = 18; + optional bool isLowRamDevice = 19; + optional int64 totalMem = 20; + optional int32 availableProcessors = 21; + repeated FeatureInfoProto featureInfos = 26; + optional int32 screenLayout = 27; //unknow enum + optional string oemkey1 = 29; + optional string buildCodeName = 30; + optional string previewSdkFingerprint = 31; +} + +message FeatureInfoProto { + optional string name = 1; + optional int32 version = 2; +} + +message ProfileInfoTemp { + optional string packageName = 1; + optional string pkgSHA1 = 2; + optional string pkgSHA256 = 3; + optional PolicyType policyTypeValue = 4; //unknow enum +} + +enum PolicyType { + UNKNOW = 0; + MANAGED_DEVICE = 1; + MANAGED_PROFILE = 2; + LEGACY_DEVICE_ADMIN = 3; +} + diff --git a/vending-app/src/main/res/values-zh-rCN/strings.xml b/vending-app/src/main/res/values-zh-rCN/strings.xml index e0999d351a..93dae0efe9 100644 --- a/vending-app/src/main/res/values-zh-rCN/strings.xml +++ b/vending-app/src/main/res/values-zh-rCN/strings.xml @@ -18,4 +18,5 @@ 如果应用出现异常,请登录您购买该应用所使用的 Google 帐号。 登录 忽略 + %s 正在下载分包 \ No newline at end of file diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index d505d6fdfd..521b81fd18 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -28,4 +28,5 @@ Forget password? Learn more Verify + %s Downloading subpackages From 5e6f01e32635c5586f9aef46b6100909aa316d42 Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Mon, 19 Aug 2024 21:33:37 +0800 Subject: [PATCH 02/11] add permission --- vending-app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 690446c4c6..52163f5e29 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + Date: Mon, 19 Aug 2024 22:05:03 +0800 Subject: [PATCH 03/11] ISplitInstallServiceCallback added --- .../ISplitInstallServiceCallback.aidl | 20 +++++++++---------- .../SplitInstallServiceImpl.kt | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl b/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl index b3952859d1..4661530d66 100644 --- a/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl +++ b/vending-app/src/main/aidl/com/google/android/play/core/splitinstall/protocol/ISplitInstallServiceCallback.aidl @@ -6,14 +6,14 @@ package com.google.android.play.core.splitinstall.protocol; interface ISplitInstallServiceCallback { - void a(int v,in Bundle bundle0) = 3; - void b(in Bundle bundle0) = 6; - void c(int v,in Bundle bundle0) = 4; - void d(in Bundle bundle0) = 9; - void e(in Bundle bundle0) = 12; - void f(in Bundle bundle0) = 13; - void g(in Bundle bundle0) = 8; - void h(int v,in Bundle bundle0) = 5; - void i(in List list0) = 7; - void j(int v,in Bundle bundle0) = 2; + oneway void onStartInstall(int status, in Bundle bundle) = 1; + oneway void onInstallCompleted(int status, in Bundle bundle) = 2; + oneway void onCancelInstall(int status, in Bundle bundle) = 3; + oneway void onGetSessionState(int status, in Bundle bundle) = 4; + oneway void onError(in Bundle bundle) = 5; + oneway void onGetSessionStates(in List list) = 6; + oneway void onDeferredUninstall(in Bundle bundle) = 7; + oneway void onDeferredInstall(in Bundle bundle) = 8; + oneway void onDeferredLanguageInstall(in Bundle bundle) = 11; + oneway void onDeferredLanguageUninstall(in Bundle bundle) = 12; } \ No newline at end of file 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 index 6fd2850d26..c200bf68b1 100644 --- 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 @@ -56,7 +56,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi trySplitInstall(pkg, splits, false) taskQueue.put(Runnable { try{ - callback.j(1, Bundle()) + callback.onStartInstall(1, Bundle()) }catch (ignored: RemoteException){ } }) @@ -90,7 +90,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi override fun getSessionStates(pkg: String, callback: ISplitInstallServiceCallback) { Log.i(TAG, "getSessionStates for package: $pkg") - callback.i(ArrayList(1)) + callback.onGetSessionStates(ArrayList(1)) } override fun splitRemoval( @@ -108,7 +108,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi callback: ISplitInstallServiceCallback ) { Log.i(TAG, "Split deferred not implemented") - callback.d(Bundle()) + callback.onDeferredInstall(Bundle()) } override fun getSessionState2( From c9bfbc040dd6beb28e3196c44f04addb43857e7e Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Wed, 21 Aug 2024 22:26:35 +0800 Subject: [PATCH 04/11] remove Phenotype / Experiment --- vending-app/build.gradle | 1 - vending-app/src/main/AndroidManifest.xml | 2 - .../android/vending/ExperimentAndConfigs.kt | 839 ------------------ .../com/android/vending/MainApplication.kt | 93 -- .../com/android/vending/PhenotypeDatabase.kt | 58 -- .../SplitInstallServiceImpl.kt | 134 +-- .../phonesky/header/PayloadsProtoStore.kt | 777 ---------------- .../phonesky/header/PhoneskyHeaderValue.kt | 585 +++++------- .../main/proto/ExperimentsAndConfigs.proto | 167 ---- .../src/main/proto/LicenseRequest.proto | 5 + .../main/proto/PhoneskyHeaderValueStore.proto | 433 --------- vending-app/src/main/proto/SplitInstall.proto | 88 ++ 12 files changed, 390 insertions(+), 2792 deletions(-) delete mode 100644 vending-app/src/main/kotlin/com/android/vending/ExperimentAndConfigs.kt delete mode 100644 vending-app/src/main/kotlin/com/android/vending/MainApplication.kt delete mode 100644 vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt delete mode 100644 vending-app/src/main/kotlin/com/google/android/phonesky/header/PayloadsProtoStore.kt delete mode 100644 vending-app/src/main/proto/ExperimentsAndConfigs.proto delete mode 100644 vending-app/src/main/proto/PhoneskyHeaderValueStore.proto create mode 100644 vending-app/src/main/proto/SplitInstall.proto diff --git a/vending-app/build.gradle b/vending-app/build.gradle index c60f8261be..c8281eef76 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -107,7 +107,6 @@ dependencies { implementation 'androidx.activity:activity-compose:1.7.2' implementation("io.coil-kt:coil-compose:2.4.0") implementation("io.coil-kt:coil-svg:2.2.2") - implementation 'org.brotli:dec:0.1.2' implementation "com.google.android.material:material:$materialVersion" implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0" diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 52163f5e29..ddcfa0e974 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -20,7 +20,6 @@ - - val applicationTags: MutableList = ArrayList() - db.rawQuery( - "SELECT partitionId, tag FROM ApplicationTags WHERE packageName = ? AND user = ? AND version = ?", - arrayOf(pkgName, account!!.name, version.toString()) - ).use { cursor -> - while (cursor.moveToNext()) { - applicationTags.add( - ApplicationTag.Builder() - .partitionId(cursor.getLong(0)) - .tag(ByteString.of(*cursor.getBlob(1))) - .build() - ) - } - } - finskyRegularInfo.applicationTagValue(applicationTags) - db.rawQuery( - "SELECT tokensTag FROM ExperimentTokens WHERE packageName = ? AND user = ? AND version = ? AND isCommitted = 0", - arrayOf(pkgName, account.name, version.toString()) - ).use { cursor -> - if (cursor.moveToNext()) { - finskyRegularInfo.tokensTag(ByteString.of(*cursor.getBlob(0))) - } - } - val experimentsInfo = buildBaseGpInfo(FINSKY_STABLE, 0).build() - var bytesTag: ByteArray? = null - db.rawQuery( - "SELECT bytesTag FROM RequestTags WHERE user = ?", - arrayOf(account.name) - ).use { cursor -> - if (cursor.moveToNext()) { - bytesTag = cursor.getBlob(0) - } - } - checkNotNull(bytesTag) - return experimentRequestData.Builder() - .deviceDataValue(deviceData) - .experimentsInfo( - List.of( - finskyRegularInfo.build(), - experimentsInfo - ) - ) - .bytesTag(ByteString.of(*bytesTag!!)) - .actionType(action.NEW_APPLICATION_SYNC) - .unknowFieldG(128) - .expPkgName(FINSKY_STABLE) - .build() - } - } catch (e: Exception) { - Log.w(TAG, "buildRequestData: ", e) - throw RuntimeException(e) - } - } - - throw RuntimeException("request experimentsandconfigs has Unknow action") - } - - private fun createExpDeviceInfo(context: Context): ExpDeviceInfo { - @SuppressLint("HardwareIds") val builder = ExpDeviceInfo.Builder() - builder.androidId( - getString(context.contentResolver, "android_id", "")!! - .toLong() - ) - builder.sdkInt(Build.VERSION.SDK_INT) - builder.buildId(Build.ID) - builder.buildDevice(Build.DEVICE) - builder.manufacturer(Build.MANUFACTURER) - builder.model(Build.MODEL) - builder.product(Build.PRODUCT) - builder.unknowEmpty("") - builder.fingerprint(Build.FINGERPRINT) - builder.country(Locale.getDefault().country) - builder.locale(Locale.getDefault().toString()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.supportAbis(Arrays.asList(*Build.SUPPORTED_ABIS)) - } - return builder.build() - } - - private fun createDeviceData(account: Account?, context: Context): DeviceData { - val expDeviceInfo = createExpDeviceInfo(context) - val expDeviceInfoWrapper = ExpDeviceInfoWrapper.Builder() - .unknowFieldb(4) - .expDeviceInfoValue(expDeviceInfo) - .build() - return DeviceData.Builder() - .hasAccount((if (account == null) 0 else 1).toLong()) - .expDeviceInfoWrapperValue(expDeviceInfoWrapper) - .unknowFlagf(false) - .unknowEmptyE(ByteString.of()) - .unknkowFieldG(DeviceDataEmptyA.Builder().build()) - .build() - } - - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - fun postRequest( - experimentRequestData: experimentRequestData, - context: Context?, - accountName: String, - token: String - ) { - try { - val url = - URL("https://www.googleapis.com/experimentsandconfigs/v1/getExperimentsAndConfigs" + "?r=" + experimentRequestData.actionType?.value + "&c=" + experimentRequestData.unknowFieldG) - - val httpURLConnection = url.openConnection() as HttpURLConnection - httpURLConnection.connectTimeout = 30000 - httpURLConnection.readTimeout = 30000 - httpURLConnection.doOutput = true - httpURLConnection.instanceFollowRedirects = false - httpURLConnection.setRequestProperty("Accept-Encoding", null) - httpURLConnection.setRequestProperty("Content-Type", "application/x-protobuf") - httpURLConnection.setRequestProperty("Content-Encoding", "gzip") - httpURLConnection.setRequestProperty("Authorization", "Bearer $token") - httpURLConnection.setRequestProperty( - "User-Agent", - "Android-Finsky/41.2.21-31 [0] [PR] 636997666 (api=3,versionCode=84122130,sdk=31,device=redroid_arm64,hardware=redroid,product=redroid_arm64,platformVersionRelease=12,model=redroid12_arm64,buildId=SQ1D.220205.004,isWideScreen=0,supportedAbis=arm64-v8a;armeabi-v7a;armeabi) (redroid_arm64 SQ1D.220205.004); gzip" - ) - val byteArrayOutputStream = ByteArrayOutputStream() - GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> - gzipOutputStream.write(experimentRequestData.encode()) - gzipOutputStream.finish() - } - val compressedData = byteArrayOutputStream.toByteArray() - httpURLConnection.outputStream.use { os -> - os.write(compressedData) - os.flush() - } - val responseCode = httpURLConnection.responseCode - Log.d(TAG, "postRequest responseCode: $responseCode") - if (responseCode >= 200 && responseCode < 300) { - val experimentResponseData = - ExperimentResponseData.ADAPTER.decode(toByteArray(httpURLConnection.inputStream)) - - PhenotypeDatabase(context).writableDatabase.use { db -> - if (experimentResponseData.bytesTag != null) { - db.rawQuery( - "SELECT user FROM RequestTags WHERE user = ?1", - arrayOf(accountName) - ).use { cursor -> - if (cursor.count > 0) { - db.execSQL( - "UPDATE RequestTags SET user = ?1, bytesTag = ?2 WHERE user = ?1", - arrayOf( - accountName, - experimentResponseData.bytesTag.toByteArray() - ) - ) - } else { - db.execSQL( - "INSERT INTO RequestTags (user, bytesTag) VALUES (?, ?)", - arrayOf( - accountName, - experimentResponseData.bytesTag.toByteArray() - ) - ) - } - } - } - for (experimentsDataWrapper in experimentResponseData.experiments) { - val experimentVersionValue = experimentsDataWrapper.experimentVersionValue - val pkgName = experimentVersionValue?.expPkgName - val version = experimentVersionValue?.version - for (expFlagsGroup in experimentsDataWrapper.expFlagsGroupValue) { - val partitionId = expFlagsGroup.applicationTagValue?.partitionId - - for (expFlag in expFlagsGroup.expFlags) { - var longValue: Long? = null - var booleValue: Long? = null - var doubleValue: Double? = null - var stringValue: String? = null - var extensionValue: ByteString? = "".encodeUtf8() - if (expFlag.valueType == null) continue - when (expFlag.valueType) { - 1 -> longValue = expFlag.longValue - 2 -> booleValue = if (expFlag.boolValue == true) 1L else 0L - 3 -> doubleValue = expFlag.doubleValue - 4 -> stringValue = expFlag.stringValue - 5 -> { - extensionValue = - if (expFlag.extensionValueValue == null) null else expFlag.extensionValueValue.mvalue - continue - } - - else -> continue - } - val flagType = 0 - db.execSQL( - "INSERT OR REPLACE INTO Flags(packageName, version, flagType, partitionId, user, name, committed, intVal, boolVal, floatVal, stringVal, extensionVal) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - arrayOf( - pkgName, - version, - flagType, - partitionId, - accountName, - expFlag.flagName, - flagType, - longValue, - booleValue, - doubleValue, - stringValue, - extensionValue!!.toByteArray() - ) - ) - } - db.execSQL( - "DELETE FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = 0", - arrayOf(pkgName, version, accountName) - ) - db.execSQL( - "INSERT INTO ExperimentTokens (packageName, version, user, isCommitted, experimentToken, serverToken, configHash, servingVersion, tokensTag, flagsHash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - arrayOf( - pkgName, - version, - accountName, - 0, - experimentsDataWrapper.experimentToken?.toByteArray(), - experimentsDataWrapper.serverToken, - calculateHash(experimentsDataWrapper).toString(), - experimentResponseData.servingVersion, - experimentsDataWrapper.tokensTag?.toByteArray(), - 0 - ) - ) - db.execSQL( - "DELETE FROM ApplicationTags WHERE packageName = ? AND version = ? AND user = ? AND partitionId = ?", - arrayOf(pkgName, version, accountName, partitionId) - ) - db.execSQL( - "INSERT OR REPLACE INTO ApplicationTags (packageName, version, partitionId, user, tag) VALUES (?, ?, ?, ?, ?)", - arrayOf( - pkgName, - version, - partitionId, - accountName, - expFlagsGroup.applicationTagValue?.tag?.toByteArray() - ) - ) - } - } - } - } - } catch (e: IOException) { - throw RuntimeException(e) - } - } - - fun buildExperimentsFlag(context: Context, accountName: String, pkgName: String) { - PhenotypeDatabase(context).readableDatabase.use { database -> - var hasFlagOverrides: Boolean - database.rawQuery("SELECT EXISTS(SELECT NULL FROM FlagOverrides)", arrayOf()) - .use { cursor -> - cursor.moveToNext() - hasFlagOverrides = cursor.getInt(0) > 0 - } - var expValue: ExperimentsValues? = null - val overFlags = ArrayList() - if (hasFlagOverrides) { - database.rawQuery( - "SELECT flagType, name, intVal, boolVal, floatVal, stringVal, extensionVal FROM FlagOverrides WHERE packageName = ? AND user = \'*\' AND committed = 0", - arrayOf(pkgName) - ).use { cursor -> - while (cursor.moveToNext()) { - overFlags.add(getFlagsValue(cursor)) - } - } - for (flag in overFlags) { - if (flag!!.name == "__phenotype_server_token" && flag.valueType == 3) { - expValue = ExperimentsValues(null, flag.stringVal, 0) - } - } - } - - database.rawQuery( - "SELECT experimentToken,serverToken,servingVersion FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = 0", - arrayOf(pkgName, version.toString(), accountName) - ).use { cursor -> - cursor.moveToNext() - if (expValue == null) { - expValue = - ExperimentsValues(cursor.getBlob(0), cursor.getString(1), cursor.getLong(2)) - } - } - val flags = TreeSet() - database.rawQuery( - "SELECT flagType, name, intVal, boolVal, floatVal, stringVal, extensionVal FROM Flags WHERE packageName = ? AND version = ? AND user = ? AND committed = 0 ORDER BY name", - arrayOf(pkgName, version.toString(), accountName) - ).use { cursor -> - while (cursor.moveToNext()) { - flags.add(getFlagsValue(cursor)) - } - } - for (flagsValue in overFlags) { - flags.remove(flagsValue) - flags.add(flagsValue) - } - val flagsValueMap = HashMap?>() - for (flag in flags) { - if (flagsValueMap[flag!!.flagType] == null) flagsValueMap[flag.flagType] = - ArrayList() - Objects.requireNonNull(flagsValueMap[flag.flagType])!!.add(flag) - } - val flagTypeList = ArrayList() - for (flagType in flagsValueMap.keys) { - flagTypeList.add( - FlagTypeValue( - flagType, Objects.requireNonNull( - flagsValueMap[flagType] - )!!.toTypedArray() - ) - ) - } - - commit(database, pkgName, accountName) - database.rawQuery( - "SELECT configHash FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = ?", - arrayOf(pkgName, version.toString(), accountName, "1") - ).use { cursor -> - cursor.moveToNext() - val configHash = cursor.getString(0) - val expeIntroduce = "$pkgName $accountName $configHash" - - val configuration = ExperimentsFlagsConfiguration( - expeIntroduce, - expValue!!.serverToken, - flagTypeList.toTypedArray(), - false, - expValue!!.experimentToken, - expValue!!.servingVersion - ) - val ExperimentsFlagsProto = buildExperimentsFlagsProto(configuration) - writeExperimentsFlag(ExperimentsFlagsProto, context, pkgName, accountName) - } - } - } - - private fun commit(database: SQLiteDatabase, pkgName: String, accountName: String) { - database.execSQL( - "INSERT OR REPLACE INTO ExperimentTokens SELECT packageName, version, user, 1 AS isCommitted, experimentToken, serverToken, configHash, servingVersion, tokensTag, flagsHash FROM ExperimentTokens WHERE packageName = ? AND version = ? AND user = ? AND isCommitted = 0", - arrayOf(pkgName, version.toString(), accountName) - ) - database.execSQL( - "DELETE FROM Flags WHERE packageName = ? AND committed = 1", - arrayOf(pkgName) - ) - database.execSQL( - "INSERT INTO Flags SELECT packageName, version, flagType, partitionId, user, name, intVal, boolVal, floatVal, stringVal, extensionVal, 1 AS committed FROM Flags WHERE packageName = ? AND version = ? AND user = ? AND committed = 0", - arrayOf(pkgName, version.toString(), accountName) - ) - } - - @JvmStatic - fun readExperimentsFlag( - context: Context, - pkgName: String, - username: String? - ): ExperimentsDataRead? { - val file = File( - context.filesDir, - if (FINSKY_REGULAR == pkgName) (if (TextUtils.isEmpty(username)) "experiment-flags-regular-null-account" else "experiment-flags-regular-" + Uri.encode( - username - )) else "experiment-flags-process-stable" - ) - if (!file.exists()) { - Log.d(TAG, "File " + file.name + " not exists") - return null - } - try { - val inputStream = DataInputStream(BufferedInputStream(FileInputStream(file))) - if (inputStream.readByte().toInt() != 1) { - throw IOException("Unrecognized file version.") - } - val result = ExperimentsDataRead() - result.setBaseToken( - inputStream.readUTF(), inputStream.readUTF(), ExperimentTokenStore.ADAPTER.decode( - Base64.decode(inputStream.readUTF(), 3) - ) - ) - var endOfFlag = 0 - while (endOfFlag == 0) { - when (inputStream.readByte()) { - 0.toByte() -> endOfFlag = 1 - 1.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readByte().toLong()) - 2.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readShort().toLong()) - 3.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readInt().toLong()) - 4.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readLong()) - 5.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readUTF()) - 6.toByte() -> { - val key = inputStream.readUTF() - val length = inputStream.readInt() - if (length >= 0) { - val value = ByteArray(length) - inputStream.readFully(value) - result.putFlag(key, value) - break - } - throw RuntimeException("Bytes flag has negative length.") - } - - 7.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readDouble()) - 8.toByte() -> result.putFlag(inputStream.readUTF(), inputStream.readBoolean()) - else -> throw RuntimeException("Unknown flag type") - } - } - inputStream.close() - return result - } catch (e: IOException) { - throw RuntimeException(e) - } - } - - private fun writeExperimentsFlag( - ExperimentsFlagsProto: ExperimentsFlagsProto, - context: Context, - pkgName: String, - username: String - ) { - try { - val file = File( - context.filesDir, - if (FINSKY_REGULAR == pkgName) (if (TextUtils.isEmpty(username)) "experiment-flags-regular-null-account" else "experiment-flags-regular-" + Uri.encode( - username - )) else "experiment-flags-process-stable" - ) - val dataOutputStream = DataOutputStream(BufferedOutputStream(FileOutputStream(file))) - dataOutputStream.writeByte(1) - dataOutputStream.writeUTF(ExperimentsFlagsProto.serverToken) - dataOutputStream.writeUTF(ExperimentsFlagsProto.expeIntroduce) - dataOutputStream.writeUTF( - Base64.encodeToString( - buildExperimentsTokenProto( - context, - username, - pkgName - ).encode(), 3 - ) - ) - for (flag in ExperimentsFlagsProto.flagValues) { - if (flag.intVal != null) { - val value = flag.intVal - if (value >= -0x80L && value <= 0x7FL) { - dataOutputStream.writeByte(1) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeByte((value.toInt())) - } else if (value >= -0x8000L && value <= 0x7FFFL) { - dataOutputStream.writeByte(2) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeShort((value.toInt())) - } else if (value >= -0x80000000L && value <= 0x7FFFFFFFL) { - dataOutputStream.writeByte(3) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeInt((value.toInt())) - } else { - dataOutputStream.writeByte(4) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeLong(value) - } - } else if (flag.boolVal != null) { - dataOutputStream.writeByte(8) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeBoolean(flag.boolVal) - } else if (flag.floatVal != null) { - dataOutputStream.writeByte(7) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeDouble(flag.floatVal) - } else if (flag.stringVal != null) { - dataOutputStream.writeByte(5) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeUTF(flag.stringVal) - } else if (flag.extensionVal != null) { - dataOutputStream.writeByte(6) - dataOutputStream.writeUTF(flag.name) - dataOutputStream.writeInt(flag.extensionVal.size) - dataOutputStream.write( - flag.extensionVal.toByteArray(), - 0, - flag.extensionVal.size - ) - } - } - Log.d(TAG, "Finished writing experiment flags into file " + file.name) - dataOutputStream.writeByte(0) - dataOutputStream.close() - } catch (e: IOException) { - throw RuntimeException(e) - } - } - - private fun buildExperimentsTokenProto( - context: Context, - user: String, - pkgName: String - ): ExperimentTokenStore { - PhenotypeDatabase(context).readableDatabase.use { db -> - db.rawQuery( - "SELECT experimentToken FROM ExperimentTokens WHERE user = ? AND packageName = ? AND version = ? AND isCommitted = 1", - arrayOf(user, pkgName, version.toString()) - ).use { cursor -> - cursor.moveToNext() - val ExperimentTokenStore_ = ExperimentTokenStore.Builder() - ExperimentTokenStore_.experimentToken = - Arrays.asList(ByteString.of(*cursor.getBlob(0))) - return ExperimentTokenStore_.build() - } - } - } - - private fun buildExperimentsFlagsProto(configuration: ExperimentsFlagsConfiguration): ExperimentsFlagsProto { - val builder = ExperimentsFlagsProto.Builder() - .expeIntroduce(configuration.expeIntroduce) - .serverToken(configuration.serverToken) - .unknowFlagB(configuration.unknowFlagB) - .servingVersion(configuration.servingVersion) - if (configuration.experimentToken != null) { - builder.experimentToken(ByteString.of(*configuration.experimentToken)) - } - val flagValueProtos = builder.flagValues.toMutableList() - for (typeValue in configuration.array) { - for (flagsValue in typeValue.values) { - flagValue2proto(flagsValue)?.let { flagValueProtos.add(it) } - } - } - builder.flagValues = flagValueProtos - return builder.build() - } - - private fun flagValue2proto(value: FlagsValue?): FlagValueProto? { - when (value!!.valueType) { - 0 -> return FlagValueProto.Builder() - .name(value.name) - .intVal(value.intVal.toLong()).build() - - 1 -> return FlagValueProto.Builder() - .name(value.name) - .boolVal(value.boolVal).build() - - 2 -> return FlagValueProto.Builder() - .name(value.name) - .floatVal(value.floatVal.toDouble()).build() - - 3 -> return FlagValueProto.Builder() - .name(value.name) - .stringVal(value.stringVal).build() - - 4 -> return FlagValueProto.Builder() - .name(value.name) - .extensionVal(ByteString.of(*value.extensionVal?:byteArrayOf())).build() - } - return null - } - - private fun getFlagsValue(cursor: Cursor): FlagsValue? { - val flagType = cursor.getInt(0) - val name = cursor.getString(1) - if (!cursor.isNull(2)) { - return FlagsValue(flagType, name, cursor.getInt(2)) - } else if (!cursor.isNull(3)) { - return FlagsValue(flagType, name, cursor.getInt(3) != 0) - } else if (!cursor.isNull(4)) { - return FlagsValue(flagType, name, cursor.getFloat(4)) - } else if (!cursor.isNull(5)) { - return FlagsValue(flagType, name, cursor.getString(5)) - } else if (!cursor.isNull(6)) { - return FlagsValue(flagType, name, cursor.getString(6)) - } - return null - } - - private fun calculateHash(experimentsDataWrapper: ExperimentsDataWrapper): Int { - var hash = 0 - for (expFlagsGroup in experimentsDataWrapper.expFlagsGroupValue) { - var applicationTag = expFlagsGroup.applicationTagValue - if (applicationTag == null) applicationTag = ApplicationTag.Builder().build() - var hashCode = applicationTag!!.partitionId.hashCode() - for (b in applicationTag.tag!!.toByteArray()) { - hashCode = hashCode * 0x1F + b - } - hash = hash * 17 xor hashCode - } - return hash - } - - @JvmStatic - fun toByteArray(inputStream: InputStream): ByteArray { - val buffer = ByteArrayOutputStream() - var nRead: Int - val data = ByteArray(1024) - - while ((inputStream.read(data, 0, data.size).also { nRead = it }) != -1) { - buffer.write(data, 0, nRead) - } - buffer.flush() - return buffer.toByteArray() - } - - - class ExperimentsDataRead { - @JvmField - var serverToken: String? = null - var expeIntroduce: String? = null - var experimentToken: ExperimentTokenStore? = null - val flagMap: MutableMap = HashMap() - - fun setBaseToken( - serverToken: String?, - expeIntroduce: String?, - experimentToken: ExperimentTokenStore? - ) { - this.serverToken = serverToken - this.expeIntroduce = expeIntroduce - this.experimentToken = experimentToken - } - - fun putFlag(name: String, value: Boolean) { - flagMap[name] = value - } - - fun putFlag(name: String, value: Long) { - flagMap[name] = value - } - - fun putFlag(name: String, value: Double) { - flagMap[name] = value - } - - fun putFlag(name: String, value: String) { - flagMap[name] = value - } - - fun putFlag(name: String, value: ByteArray) { - flagMap[name] = value - } - } - - internal class ExperimentsFlagsConfiguration( - val expeIntroduce: String, - val serverToken: String?, - val array: Array, - val unknowFlagB: Boolean, - val experimentToken: ByteArray?, - val servingVersion: Long - ) - - internal class FlagTypeValue(private val flagType: Int, val values: Array) - - internal class ExperimentsValues( - var experimentToken: ByteArray?, - var serverToken: String?, - var servingVersion: Long - ) - - class FlagsValue : Comparable { - val flagType: Int - val name: String - var intVal: Int = 0 - var boolVal: Boolean = false - var floatVal: Float = 0f - var stringVal: String? = null - var extensionVal: ByteArray? = null - var valueType: Int - - constructor(flagType: Int, name: String, intVal: Int) { - this.valueType = 0 - this.flagType = flagType - this.name = name - this.intVal = intVal - } - - constructor(flagType: Int, name: String, boolVal: Boolean) { - this.valueType = 1 - this.flagType = flagType - this.name = name - this.boolVal = boolVal - } - - constructor(flagType: Int, name: String, floatVal: Float) { - this.valueType = 2 - this.flagType = flagType - this.name = name - this.floatVal = floatVal - } - - constructor(flagType: Int, name: String, stringVal: String?) { - this.valueType = 3 - this.flagType = flagType - this.name = name - this.stringVal = stringVal - } - - override fun compareTo(flagValue: FlagsValue?): Int { - if (flagValue == null) { - return -1 - } - return name.compareTo(flagValue?.name!!) - } - - val value: Any? - get() { - when (this.valueType) { - 0 -> return intVal - 1 -> return boolVal - 2 -> return floatVal - 3 -> return stringVal - 4 -> return extensionVal - } - return null - } - - override fun equals(obj: Any?): Boolean { - if (this.valueType == (obj as FlagsValue?)!!.valueType) { - when (this.valueType) { - 0 -> return this.intVal == obj!!.intVal - 1 -> return this.boolVal == obj!!.boolVal - 2 -> return this.floatVal == obj!!.floatVal - 3 -> return this.stringVal == obj!!.stringVal - 4 -> return this.extensionVal == obj!!.extensionVal - } - } - return false - } - } -} diff --git a/vending-app/src/main/kotlin/com/android/vending/MainApplication.kt b/vending-app/src/main/kotlin/com/android/vending/MainApplication.kt deleted file mode 100644 index 4ab2766dc4..0000000000 --- a/vending-app/src/main/kotlin/com/android/vending/MainApplication.kt +++ /dev/null @@ -1,93 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ -package com.android.vending - -import android.accounts.AccountManager -import android.accounts.AuthenticatorException -import android.accounts.OperationCanceledException -import android.app.ActivityManager -import android.app.Application -import android.content.Context -import android.os.Build -import android.os.Process -import android.util.Log -import com.google.android.phonesky.header.PayloadsProtoStore -import com.google.android.phonesky.header.PhoneskyHeaderValue -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE -import org.microg.vending.billing.FINSKY_REGULAR -import org.microg.vending.billing.FINSKY_STABLE -import java.io.IOException -import java.util.concurrent.TimeUnit - -class MainApplication : Application() { - override fun onCreate() { - super.onCreate() - - val accountManager = AccountManager.get(this) - val accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE) - - if (isMainProcess() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && accounts.isNotEmpty()) { - GlobalScope.launch(Dispatchers.IO) { - val payloads = PayloadsProtoStore.readCache(applicationContext) - if (payloads == null || payloads.mvalue.isEmpty()) PayloadsProtoStore.cachePayload(accounts[0], applicationContext) - for (account in accounts) { - var token : String? - val accountName = account.name - val future = accountManager.getAuthToken( - account, - "oauth2:https://www.googleapis.com/auth/experimentsandconfigs", - null, - false, - null, - null - ) - try { - val bundle = future.getResult(2, TimeUnit.SECONDS) - token = bundle.getString(AccountManager.KEY_AUTHTOKEN) - } catch (e: Exception) { - return@launch - } - - if (token == null) { - Log.d("MainApplication", "onCreate token is null") - return@launch - } - - ExperimentAndConfigs.postRequest( - ExperimentAndConfigs.buildRequestData(this@MainApplication), this@MainApplication, accountName, token) - ExperimentAndConfigs.postRequest( - ExperimentAndConfigs.buildRequestData(this@MainApplication, "NEW_APPLICATION_SYNC", FINSKY_REGULAR, account), this@MainApplication, accountName, token) - ExperimentAndConfigs.postRequest( - ExperimentAndConfigs.buildRequestData(this@MainApplication, "NEW_USER_SYNC_ALL_ACCOUNT", null, null), this@MainApplication, "", token) - ExperimentAndConfigs.buildExperimentsFlag(this@MainApplication, accountName, FINSKY_REGULAR) - ExperimentAndConfigs.buildExperimentsFlag(this@MainApplication, "", FINSKY_REGULAR) - ExperimentAndConfigs.buildExperimentsFlag(this@MainApplication, accountName, FINSKY_STABLE) - } - try { - PhoneskyHeaderValue.getPhoneskyHeader(this@MainApplication, accounts[0]) - } catch (e: IOException) { - throw RuntimeException(e) - } catch (e: IllegalAccessException) { - throw RuntimeException(e) - } catch (e: AuthenticatorException) { - throw RuntimeException(e) - } catch (e: OperationCanceledException) { - throw RuntimeException(e) - } - } - } - } - - private fun isMainProcess(): Boolean { - val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val processInfo = am.runningAppProcesses ?: return false - val mainProcessName = packageName - val myPid = Process.myPid() - return processInfo.any { it.pid == myPid && it.processName == mainProcessName } - } -} diff --git a/vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt b/vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt deleted file mode 100644 index f6d3350cf0..0000000000 --- a/vending-app/src/main/kotlin/com/android/vending/PhenotypeDatabase.kt +++ /dev/null @@ -1,58 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ -package com.android.vending - -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper - -class PhenotypeDatabase(context: Context?) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { - - companion object { - const val DATABASE_NAME = "phenotype.db" - const val DATABASE_VERSION = 0x20 - } - - override fun onCreate(db: SQLiteDatabase) { - db.execSQL(" CREATE TABLE IF NOT EXISTS Packages(\n packageName TEXT NOT NULL PRIMARY KEY,\n version INTEGER NOT NULL,\n params BLOB,\n dynamicParams BLOB,\n weak INTEGER NOT NULL,\n androidPackageName TEXT NOT NULL,\n isSynced INTEGER,\n serializedDeclarativeRegInfo BLOB DEFAULT NULL,\n configTier INTEGER DEFAULT NULL,\n baselineCl INTEGER DEFAULT NULL,\n heterodyneInfo BLOB DEFAULT NULL,\n runtimeProperties BLOB DEFAULT NULL,\n declarativeRegistrationInfo BLOB DEFAULT NULL\n )\n") - db.execSQL("CREATE INDEX IF NOT EXISTS androidPackageName ON Packages (androidPackageName)") - db.execSQL(" CREATE TABLE IF NOT EXISTS ApplicationStates(\n packageName TEXT NOT NULL PRIMARY KEY,\n user TEXT NOT NULL,\n version INTEGER NOT NULL,\n patchable INTEGER\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS MultiCommitApplicationStates(\n packageName TEXT NOT NULL,\n user TEXT NOT NULL,\n version INTEGER NOT NULL,\n PRIMARY KEY(packageName, user)\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS LogSources(\n logSourceName TEXT NOT NULL,\n packageName TEXT NOT NULL,\n PRIMARY KEY(logSourceName, packageName)\n )\n") - db.execSQL("CREATE INDEX IF NOT EXISTS packageName ON LogSources(packageName)") - db.execSQL(" CREATE TABLE IF NOT EXISTS WeakExperimentIds(\n packageName TEXT NOT NULL,\n experimentId INTEGER NOT NULL\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS ExperimentTokens(\n packageName TEXT NOT NULL,\n version INTEGER NOT NULL,\n user TEXT NOT NULL,\n isCommitted INTEGER NOT NULL,\n experimentToken BLOB NOT NULL,\n serverToken TEXT NOT NULL,\n configHash TEXT NOT NULL DEFAULT \'\',\n servingVersion INTEGER NOT NULL DEFAULT 0,\n tokensTag BLOB DEFAULT NULL,\n flagsHash INTEGER DEFAULT NULL,\n PRIMARY KEY(packageName, version, user, isCommitted)\n )\n") - db.execSQL("CREATE INDEX IF NOT EXISTS committed ON ExperimentTokens(packageName, version, user, isCommitted)") - db.execSQL(" CREATE TABLE IF NOT EXISTS ExternalExperimentTokens(\n packageName TEXT NOT NULL PRIMARY KEY,\n experimentToken BLOB NOT NULL\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS Flags(\n packageName TEXT NOT NULL,\n version INTEGER NOT NULL,\n flagType INTEGER NOT NULL,\n partitionId INTEGER NOT NULL,\n user TEXT NOT NULL,\n name TEXT NOT NULL,\n intVal INTEGER,\n boolVal INTEGER,\n floatVal REAL,\n stringVal TEXT,\n extensionVal BLOB,\n committed INTEGER NOT NULL,\n PRIMARY KEY(packageName, version, flagType, partitionId, user, name, committed)\n );\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS RequestTags(\n user TEXT NOT NULL PRIMARY KEY,\n bytesTag BLOB NOT NULL\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS ApplicationTags(\n packageName TEXT NOT NULL,\n version INTEGER NOT NULL,\n partitionId INTEGER NOT NULL,\n user TEXT NOT NULL,\n tag BLOB NOT NULL,\n PRIMARY KEY(packageName, version, partitionId, user)\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS CrossLoggedExperimentTokens(\n fromPackageName TEXT NOT NULL,\n fromVersion INTEGER NOT NULL,\n fromUser TEXT NOT NULL,\n toPackageName TEXT NOT NULL,\n toVersion INTEGER NOT NULL,\n isCommitted INTEGER NOT NULL,\n token BLOB NOT NULL,\n provenance INTEGER NOT NULL\n )\n") - db.execSQL(" CREATE INDEX IF NOT EXISTS apply ON CrossLoggedExperimentTokens(\n fromPackageName,\n fromVersion,\n fromUser,\n toPackageName,\n toVersion,\n isCommitted\n )\n") - db.execSQL("CREATE INDEX IF NOT EXISTS remove ON CrossLoggedExperimentTokens(toPackageName)") - db.execSQL(" CREATE TABLE IF NOT EXISTS ChangeCounts(\n packageName TEXT NOT NULL PRIMARY KEY,\n count INTEGER NOT NULL\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS DogfoodsToken(\n \"key\" INTEGER NOT NULL PRIMARY KEY,\n token BLOB\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS LastFetch(\n \"key\" INTEGER NOT NULL PRIMARY KEY,\n servertimestamp INTEGER NOT NULL\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS FlagOverrides(\n packageName TEXT NOT NULL,\n user TEXT NOT NULL,\n name TEXT NOT NULL,\n flagType INTEGER NOT NULL,\n intVal INTEGER,\n boolVal INTEGER,\n floatVal REAL,\n stringVal TEXT,\n extensionVal BLOB,\n committed,\n PRIMARY KEY(packageName, user, name, committed)\n );\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS LastSyncAfterRequest(\n packageName TEXT NOT NULL PRIMARY KEY,\n servingVersion INTEGER NOT NULL DEFAULT 0,\n androidPackageName TEXT DEFAULT NULL\n )\n") - db.execSQL(" CREATE TABLE IF NOT EXISTS StorageInfos (\n androidPackageName TEXT UNIQUE NOT NULL,\n secret BLOB NOT NULL,\n deviceEncryptedSecret BLOB NOT NULL\n )\n") - db.execSQL(" CREATE TABLE AppWideProperties (\n androidPackageName TEXT UNIQUE NOT NULL,\n appWideProperties BLOB NOT NULL\n );\n") - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - } - - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { - super.onDowngrade(db, oldVersion, newVersion) - db.execSQL("DROP TABLE IF EXISTS android_packages;") - db.execSQL("DROP TABLE IF EXISTS config_packages;") - db.execSQL("DROP TABLE IF EXISTS config_packages_to_log_sources;") - db.execSQL("DROP TABLE IF EXISTS cross_logged_tokens;") - db.execSQL("DROP TABLE IF EXISTS flag_overrides;") - db.execSQL("DROP TABLE IF EXISTS log_sources;") - db.version = newVersion - db.setForeignKeyConstraintsEnabled(true) - } -} 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 index c200bf68b1..084af400e4 100644 --- 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 @@ -14,6 +14,7 @@ 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.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -24,28 +25,27 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import com.android.vending.R -import com.google.android.phonesky.header.PhoneskyHeaderValue.GoogleApiRequest -import com.google.android.phonesky.header.PhoneskyValue -import com.google.android.phonesky.header.RequestLanguagePkg +import com.android.vending.RequestLanguagePackage +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.brotli.dec.BrotliInputStream 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.IOException -import java.io.InputStream -import java.io.OutputStream import java.net.HttpURLConnection import java.net.URL import java.nio.file.Files +import java.nio.file.Paths 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, @@ -211,20 +211,19 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi return false } try { - val downloadTempFile = File(context.filesDir, "phonesky-download-service/temp") - if (!downloadTempFile.parentFile.exists()) { - downloadTempFile.parentFile.mkdirs() + if(!tempFilePath.exists()){ + tempFilePath.mkdir() } val language:String? = if (langName.isNotEmpty()) { langName[0] } else { null } - for (downloadUrl in downloadUrls) { - taskQueue.put(Runnable { - installSplitPackage(downloadUrl, downloadTempFile, packageName, language) - }) - } + + taskQueue.put(Runnable { + installSplitPackage(downloadUrls, packageName, language) + }) + return true } catch (e: Exception) { @@ -234,41 +233,38 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } @RequiresApi(Build.VERSION_CODES.O) - private fun downloadSplitPackage(downloadUrl: String, downloadTempFile: File) : Boolean{ - Log.d(TAG, "downloadSplitPackage downloadUrl:$downloadUrl") - val url = URL(downloadUrl) - val connection = url.openConnection() as HttpURLConnection - connection.readTimeout = 30000 - connection.connectTimeout = 30000 - connection.requestMethod = "GET" - if (connection.responseCode == HttpURLConnection.HTTP_OK) { - val tempFile: OutputStream = - BufferedOutputStream(Files.newOutputStream(downloadTempFile.toPath())) - val inputStream: InputStream = BufferedInputStream(connection.inputStream) - val buffer = ByteArray(4096) - var bytesRead: Int - - while ((inputStream.read(buffer).also { bytesRead = it }) != -1) { - Log.d(TAG, "downloadSplitPackage: $bytesRead") - tempFile.write(buffer, 0, bytesRead) + 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(Files.newOutputStream(Paths.get(tempFilePath.toString(),downloadUrl[0]))).use { outputstream -> + inputstream.copyTo(outputstream) + } + } + }else{ + stat = false } - inputStream.close() - tempFile.close() + Log.d(TAG, "downloadSplitPackage code: " + connection.responseCode) } - Log.d(TAG, "downloadSplitPackage code: " + connection.responseCode) - return connection.responseCode == HttpURLConnection.HTTP_OK + return stat } @RequiresApi(Build.VERSION_CODES.O) private fun installSplitPackage( - downloadUrl: String, - downloadTempFile: File, + downloadUrl: ArrayList>, packageName: String, language: String? ) { try { Log.d(TAG, "installSplitPackage downloadUrl:$downloadUrl") - if (downloadSplitPackage(downloadUrl, downloadTempFile)) { + if (downloadSplitPackage(downloadUrl)) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.cancel(NOTIFY_ID) val packageInstaller: PackageInstaller @@ -292,19 +288,21 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } val sessionId: Int + var session : Session? = null + var totalDownloaded = 0L try { sessionId = packageInstaller.createSession(params) - val session = packageInstaller.openSession(sessionId) + session = packageInstaller.openSession(sessionId) + try { - val buffer = ByteArray(4096) - var bytesRead: Int - BrotliInputStream(FileInputStream(downloadTempFile)).use { `in` -> - session.openWrite("MGSplitPackage", 0, -1).use { out -> - while ((`in`.read(buffer).also { bytesRead = it }) != -1) { - out.write(buffer, 0, bytesRead) - } - session.fsync(out) + downloadUrl.forEach { item -> + val pkgPath = File(tempFilePath.toString(),item[0]) + session.openWrite(item[0], 0, -1).use { outputstream -> + Files.newInputStream(pkgPath.toPath()).copyTo(outputstream) + session.fsync(outputstream) } + totalDownloaded += pkgPath.length() + pkgPath.delete() } } catch (e: Exception) { Log.e(TAG, "Error installing split", e) @@ -313,11 +311,14 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi 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 { @@ -334,9 +335,9 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi langName: Array, splitName: Array, versionCode: Long - ): ArrayList { + ): ArrayList> { Log.d(TAG, "getDownloadUrls: ") - val downloadUrls = ArrayList() + val downloadUrls = ArrayList>() try { val requestUrl = StringBuilder( "https://play-fe.googleapis.com/fdfe/delivery?doc=" + packageName + "&ot=1&vc=" + versionCode + "&bvc=" + versionCode + @@ -356,23 +357,22 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi val googleApiRequest = GoogleApiRequest( requestUrl.toString(), "GET", accounts[0], context, - PhoneskyValue.Builder().languages( - RequestLanguagePkg.Builder().language(langName.filterNotNull()).build() - ).build() + RequestLanguagePackage.Builder().language(langName.filterNotNull()).build() ) val response = googleApiRequest.sendRequest(null) val pkgs = response?.fdfeApiResponseValue?.splitReqResult?.pkgList?.pkgDownlaodInfo + Log.w(TAG, "response?.fdfeApiResponseValue?.splitReqResult?.pkgList?.pkgDownlaodInfo:" + pkgs); if (pkgs != null) { for (item in pkgs) { for (lang in langName) { if (("config.$lang") == item.splitPkgName) { - downloadUrls.add(item.slaveDownloadInfo!!.url!!) + downloadUrls.add(arrayOf(lang!!, item.downloadUrl1!!)) } } Log.d(TAG, "requestSplitsPackage: $splitName") for (split in splitName) { if (split != null && split == item.splitPkgName) { - downloadUrls.add(item.slaveDownloadInfo!!.url!!) + downloadUrls.add(arrayOf(split, item.downloadUrl1!!)) } } } @@ -392,10 +392,14 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi when (status) { PackageInstaller.STATUS_SUCCESS -> { if (taskQueue.isNotEmpty()) { - taskQueue.take().run() + thread { + taskQueue.take().run() + } + } + if(taskQueue.size <= 1){ + NotificationManagerCompat.from(context).cancel(1) + sendCompleteBroad(context, intent) } - if(taskQueue.size <= 1) NotificationManagerCompat.from(context).cancel(1) - sendCompleteBroad(context, intent.getStringExtra("pkg")?:"", intent.getStringExtra("language")) } PackageInstaller.STATUS_FAILURE -> { @@ -414,6 +418,8 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi else -> { taskQueue.clear() NotificationManagerCompat.from(context).cancel(1) + val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + Log.d("InstallResultReceiver", errorMsg ?: "") Log.w(TAG, "onReceive: install fail") } } @@ -424,17 +430,17 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } } - private fun sendCompleteBroad(context: Context, pkg: String, split: String?) { - Log.d(TAG, "sendCompleteBroad: $pkg") + private fun sendCompleteBroad(context: Context, intent: Intent) { + Log.d(TAG, "sendCompleteBroad: $intent") val extra = Bundle() extra.putInt("status", 5) - extra.putLong("total_bytes_to_download", 99999) - extra.putString("languages", split) + extra.putLong("total_bytes_to_download", intent.getLongExtra("bytes_downloaded",0)) + extra.putString("languages", intent.getStringExtra("language")) extra.putInt("error_code", 0) extra.putInt("session_id", 0) - extra.putLong("bytes_downloaded", 99999) + extra.putLong("bytes_downloaded", intent.getLongExtra("bytes_downloaded",0)) val intent = Intent("com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService") - intent.setPackage(pkg) + intent.setPackage(intent.getStringExtra("pkg")) intent.putExtra("session_state", extra) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) diff --git a/vending-app/src/main/kotlin/com/google/android/phonesky/header/PayloadsProtoStore.kt b/vending-app/src/main/kotlin/com/google/android/phonesky/header/PayloadsProtoStore.kt deleted file mode 100644 index 90e02aac8c..0000000000 --- a/vending-app/src/main/kotlin/com/google/android/phonesky/header/PayloadsProtoStore.kt +++ /dev/null @@ -1,777 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2024 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ -package com.google.android.phonesky.header - -import android.accounts.Account -import android.accounts.AccountManager -import android.annotation.SuppressLint -import android.app.ActivityManager -import android.app.admin.DevicePolicyManager -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.graphics.Point -import android.opengl.GLES10 -import android.os.Build -import android.telephony.TelephonyManager -import android.text.TextUtils -import android.util.Base64 -import android.util.DisplayMetrics -import android.util.Log -import android.view.WindowManager -import org.microg.vending.billing.GServices.getString -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.util.Arrays -import java.util.Objects -import java.util.Random -import java.util.regex.Pattern -import java.util.stream.Collectors -import javax.microedition.khronos.egl.EGL10 -import javax.microedition.khronos.egl.EGLConfig -import javax.microedition.khronos.egl.EGLContext -import javax.microedition.khronos.egl.EGLDisplay -import javax.microedition.khronos.egl.EGLSurface - -object PayloadsProtoStore { - private val TAG: String = PayloadsProtoStore::class.java.simpleName - private const val FILE_NAME = "finsky/shared/payload_valuestore.pb" - - fun accountSha256(account: Account, context: Context): String? { - try { - val androidId = getString(context.contentResolver, "android_id", "") - val androidIdAcc = (androidId + "-" + account.name).toByteArray() - val messageDigest0 = MessageDigest.getInstance("SHA256") - messageDigest0.update(androidIdAcc, 0, androidIdAcc.size) - return Base64.encodeToString(messageDigest0.digest(), 11) - } catch (ignored: Exception) { - return null - } - } - - @JvmStatic - fun readCache(context: Context): SyncReqWrapper? { - Log.d(TAG, "readCache: ") - val cacheFile = File(context.filesDir, FILE_NAME) - if (!cacheFile.exists()) { - return null - } - try { - FileInputStream(cacheFile).use { inputStream -> - return SyncReqWrapper.ADAPTER.decode(inputStream) - } - } catch (e: IOException) { - Log.w(TAG, "Error reading person from file", e) - return null - } - } - - fun cachePayload(account: Account, context: Context) { - Log.d(TAG, "cachePayload: ") - val builder = SyncReqWrapper.Builder() - val payloads = buildPayloads(account, context) - for (payload in payloads) { - if (payload != null) { - builder.mvalue = builder.mvalue.toMutableList().apply { add(payload) } - } - } - val cacheFile = File(context.filesDir, FILE_NAME) - try { - if (!cacheFile.exists()) { - if (!cacheFile.parentFile.exists()) cacheFile.parentFile.mkdirs() - cacheFile.createNewFile() - } - } catch (e: Exception) { - Log.w(TAG, "Create payload_valuestore.pb failed !") - return - } - try { - FileOutputStream(cacheFile).use { outputStream -> - outputStream.write(builder.build().encode()) - Log.d(TAG, "Person written to file: " + cacheFile.absolutePath) - } - } catch (e: IOException) { - Log.w(TAG, "Error writing person to file", e) - } - } - - private fun generateRandomIMEI(): String { - val random = Random() - - // Generate the first 14 random digits - val imeiBuilder = StringBuilder() - for (i in 0..13) { - val digit = random.nextInt(10) - imeiBuilder.append(digit) - } - - // Calculate the check digit - val imei14 = imeiBuilder.toString() - val checkDigit = calculateLuhnCheckDigit(imei14) - - // Splice into a complete IMEI - imeiBuilder.append(checkDigit) - return imeiBuilder.toString() - } - - private fun calculateLuhnCheckDigit(imei14: String): Int { - var sum = 0 - for (i in 0 until imei14.length) { - var digit = Character.getNumericValue(imei14[i]) - if (i % 2 == 1) { - digit *= 2 - } - if (digit > 9) { - digit -= 9 - } - sum += digit - } - return (10 - (sum % 10)) % 10 - } - - private fun buildPayloads(account: Account, context: Context): Array { - val gpuInfos: ArrayList = fetchGLInfo() ?: return arrayOfNulls(0) - //---------------------------------------GPU info-------------------------------------------------------------------- - val accountSha256 = accountSha256(account, context) - val accountAssValue = AccountAssValue.Builder().mvalue(accountSha256).build() - val accountAossiationPayload = - AccountAossiationPayload.Builder().mvalue(accountAssValue).build() - val accountAossiationPayloadRequest = - SyncRequest.Builder().AccountAossiationPayloadVALUE(accountAossiationPayload).build() - //-------------------------------------------------------------------------------------------------------------------- - val carrierPropertiesPayloadRequest = createCarrierPropertiesPayloadRequest(context) - - val deviceAccountsPayloadRequest = createDeviceAccountsPayloadRequest(context) - - val deviceInfoCollect = createDeviceInfoCollect(context, gpuInfos.filterNotNull()) - - val deviceCapabilitiesPayloadRequest = - createDeviceCapabilitiesPayloadRequest(deviceInfoCollect) - - val deviceInputPropertiesPayloadRequest = - createDeviceInputPropertiesPayloadRequest(deviceInfoCollect) - - val deviceModelPayloadRequest = createDeviceModelPayloadRequest() - - val enterprisePropertiesPayloadRequest = createEnterprisePropertiesPayloadRequest(context) - - val hardwareIdentifierPayloadRequest = createHardwareIdentifierPayloadRequest(context) - - val hardwarePropertiesPayloadRequest = - createHardwarePropertiesPayloadRequest(deviceInfoCollect) - - val localePropertiesPayloadRequest = createLocalePropertiesPayloadRequest() - - val playPartnerPropertiesPayloadRequest = createPlayPartnerPropertiesPayloadRequest() - - val playPropertiesPayloadRequest = createPlayPropertiesPayload(context) - - val screenPropertiesPayloadRequest = createScreenPropertiesPayloadRequest(deviceInfoCollect) - - val systemPropertiesPayloadRequest = createSystemPropertiesPayloadRequest(deviceInfoCollect) - - val gpuPayloadRequest = createGpuPayloadRequest(gpuInfos.filterNotNull()) - - return arrayOf( - accountAossiationPayloadRequest, - carrierPropertiesPayloadRequest, - deviceAccountsPayloadRequest, - deviceCapabilitiesPayloadRequest, - deviceInputPropertiesPayloadRequest, - deviceModelPayloadRequest, - enterprisePropertiesPayloadRequest, - hardwareIdentifierPayloadRequest, - hardwarePropertiesPayloadRequest, - localePropertiesPayloadRequest, // NOTIFICATION_ROUTING_INFO_PAYLOAD, - playPartnerPropertiesPayloadRequest, - playPropertiesPayloadRequest, - screenPropertiesPayloadRequest, - systemPropertiesPayloadRequest, - gpuPayloadRequest - ) - } - - private fun createCarrierPropertiesPayloadRequest(context: Context): SyncRequest? { - var carrierPropertiesPayloadRequest: SyncRequest? = null - try { - val telephonyManager = - context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - @SuppressLint("HardwareIds") val subscriberId1 = - (telephonyManager.subscriberId.toLong() / 100000L).toString() + "00000" - val groupIdLevel = telephonyManager.groupIdLevel1 - val simOperator = telephonyManager.simOperator - val operatorName = telephonyManager.simOperatorName - var simcardId = 0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - simcardId = telephonyManager.simCarrierId - } - var carrierIdFromSimMccMnc = 0 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - carrierIdFromSimMccMnc = telephonyManager.carrierIdFromSimMccMnc - } - - val telephonyInfo = TelephonyInfo.Builder().subscriberId1(subscriberId1.toLong()) - .operatorName(operatorName).groupidLevel(groupIdLevel).simcardId(simcardId) - .CarrierIdFromSimMccMnc(carrierIdFromSimMccMnc).build() - - val telephonyStateWrapper = - TelephonyStateWrapper.Builder().mvalue(telephonyInfo).build() - val carrierPropertiesPayload = CarrierPropertiesPayload.Builder() - .telephonyStateValue(telephonyStateWrapper).simOperator(simOperator).build() - carrierPropertiesPayloadRequest = - SyncRequest.Builder().CarrierPropertiesPayloadVALUE(carrierPropertiesPayload) - .build() - } catch (securityException: SecurityException) { - Log.w(TAG, "SecurityException when reading IMSI.", securityException) - } catch (stateException: IllegalStateException) { - Log.w( - TAG, - "IllegalStateException when reading IMSI. This is a known SDK 31 Samsung bug.", - stateException - ) - } - return carrierPropertiesPayloadRequest - } - - private fun createDeviceAccountsPayloadRequest(context: Context): SyncRequest { - val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager - val accounts = accountManager.accounts - - val builder = DeviceAccountsPaylaod.Builder() - for (account in accounts) { - builder.mvalue = builder.mvalue.toMutableList().apply { - add( - AccountAssValue.Builder().mvalue(accountSha256(account, context)).build() - ) - } - } - return SyncRequest.Builder().DeviceAccountsPaylaodVALUE(builder.build()).build() - } - - private fun createDeviceInfoCollect( - context: Context, - gpuInfos: List - ): DeviceInfoCollect { - val builder = DeviceInfoCollect.Builder() - builder.reqTouchScreen(0).reqKeyboardType(0).reqNavigation(0).desityDeviceStablePoint(0) - .reqInputFeatures1(false) - .reqInputFeatures2(false).desityDeviceStable(0).reqGlEsVersion(0) - - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val configurationInfo = activityManager.deviceConfigurationInfo - if (configurationInfo != null) { - if (configurationInfo.reqTouchScreen != Configuration.TOUCHSCREEN_UNDEFINED) { - builder.reqTouchScreen(configurationInfo.reqTouchScreen) - } - if (configurationInfo.reqKeyboardType != Configuration.KEYBOARD_UNDEFINED) { - builder.reqKeyboardType(configurationInfo.reqKeyboardType) - } - if (configurationInfo.reqNavigation != Configuration.NAVIGATION_UNDEFINED) { - builder.reqNavigation(configurationInfo.reqNavigation) - } - builder.reqGlEsVersion(configurationInfo.reqGlEsVersion) - builder.reqInputFeatures1((configurationInfo.reqInputFeatures and 1) == 1) - .reqInputFeatures2( - (configurationInfo.reqInputFeatures and 2) > 0 - ) - } - - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - val size = Point() - if (windowManager != null) { - val display = windowManager.defaultDisplay - display.getSize(size) - - builder.displaySizex(size.x).displaySizey(size.y) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.desityDeviceStable(DisplayMetrics.DENSITY_DEVICE_STABLE) - .desityDeviceStablePoint( - calculatePoint(size, DisplayMetrics.DENSITY_DEVICE_STABLE) - ) - } - - val configuration = context.resources.configuration - builder.screenLayout(configuration.screenLayout) - .smallestScreenWidthDp(configuration.smallestScreenWidthDp) - .systemSharedLibraryNames(Arrays.asList(*Objects.requireNonNull(context.packageManager.systemSharedLibraryNames))) - .locales(Arrays.asList(*context.assets.locales)) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.glExtensions( - gpuInfos.stream() - .flatMap { fetchedGlStrings: FetchedGlStrings -> Arrays.stream(fetchedGlStrings.glExtensions) } - .collect(Collectors.toList())) - .isLowRamDevice(activityManager.isLowRamDevice) - } - - val memoryInfo = ActivityManager.MemoryInfo() - activityManager.getMemoryInfo(memoryInfo) - builder.totalMem(memoryInfo.totalMem) - .availableProcessors(Runtime.getRuntime().availableProcessors()) - - val systemAvailableFeatures = context.packageManager.systemAvailableFeatures - for (featureInfo in systemAvailableFeatures) { - if (!TextUtils.isEmpty(featureInfo.name)) { - var featureInfoProto = FeatureInfoProto.Builder().build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - featureInfoProto = FeatureInfoProto.Builder().name(featureInfo.name) - .version(featureInfo.version).build() - } - builder.featureInfos = builder.featureInfos.toMutableList().apply { - add(featureInfoProto) - } - builder.featureNames = builder.featureNames.toMutableList().apply { - add(featureInfoProto.name!!) - } - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - builder.supportedAbis(java.util.List.of(*Build.SUPPORTED_ABIS)) - } - - var prop = getSystemProperty("ro.oem.key1", "") - if (!TextUtils.isEmpty(prop)) { - builder.oemkey1(prop) - } - builder.buildCodeName(Build.VERSION.CODENAME) - prop = getSystemProperty("ro.build.version.preview_sdk_fingerprint", "") - if (!TextUtils.isEmpty(prop)) { - builder.previewSdkFingerprint(prop) - } - return builder.build() - } - - private fun createDeviceCapabilitiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val builder = DeviceCapabilitiesPayload.Builder() - builder.glExtensions(deviceInfoCollect.glExtensions) - for (featureInfoProto in deviceInfoCollect.featureInfos) { - builder.featureInfos = builder.featureInfos.toMutableList().apply { - add( - FeatureInfoProto.Builder().name(featureInfoProto.name) - .version(featureInfoProto.version).build() - ) - } - } - builder.systemSharedLibraryNames(deviceInfoCollect.systemSharedLibraryNames) - .locales(deviceInfoCollect.locales).unknowFlag(false) - return SyncRequest.Builder().DeviceCapabilitiesPayloadVALUE(builder.build()).build() - } - - private fun createDeviceInputPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val builder = DeviceInputPropertiesPayload.Builder() - builder.reqInputFeatures1(deviceInfoCollect.reqInputFeatures1) - .reqKeyboardType(deviceInfoCollect.reqKeyboardType) - .reqNavigation(deviceInfoCollect.reqNavigation) - return SyncRequest.Builder().DeviceInputPropertiesPayloadVALUE(builder.build()).build() - } - - private fun createDeviceModelPayloadRequest(): SyncRequest { - val builder = DeviceModelPayload.Builder() - builder.MANUFACTURER(Build.MANUFACTURER).MODEL(Build.MODEL).DEVICE(Build.DEVICE).PRODUCT( - Build.PRODUCT - ).BRAND(Build.BRAND) - return SyncRequest.Builder().DeviceModelPayloadVALUE(builder.build()).build() - } - - private fun createEnterprisePropertiesPayloadRequest(context: Context): SyncRequest { - val enterprisePropertiesPayload = EnterprisePropertiesPayload.Builder() - val devicePolicyManager = - context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val activeAdmins = devicePolicyManager.activeAdmins - if (activeAdmins != null) { - for (componentName in activeAdmins) { - val packageName = componentName.packageName - var packageInfo: PackageInfo? = null - try { - packageInfo = context.packageManager.getPackageInfo( - packageName, - PackageManager.GET_SIGNATURES - ) - } catch (ignored: Exception) { - } - - val isDeviceOwner = devicePolicyManager.isDeviceOwnerApp(packageName) - var isProfileOwner = false - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - isProfileOwner = devicePolicyManager.isProfileOwnerApp(packageName) - } - - val profileInfoTemp = - ProfileInfoTemp.Builder().packageName(componentName.packageName) - .policyTypeValue(if (isDeviceOwner) PolicyType.MANAGED_DEVICE else if (isProfileOwner) PolicyType.MANAGED_PROFILE else PolicyType.LEGACY_DEVICE_ADMIN) - .pkgSHA1(calculateSHA(packageInfo!!.signatures[0].toByteArray(), "SHA1")) - .pkgSHA256(calculateSHA(packageInfo.signatures[0].toByteArray(), "SHA256")) - .build() - val profileInfo = ProfileInfo.Builder().pkgName(profileInfoTemp.packageName) - .pkgSHA1(profileInfoTemp.pkgSHA1).pkgSHA256(profileInfoTemp.pkgSHA256) - .policyTypeValue(MangedScope.fromValue(profileInfoTemp.policyTypeValue!!.value)) - .build() - if (isProfileOwner) { - enterprisePropertiesPayload.profileOwner(profileInfo) - } - enterprisePropertiesPayload.mdefault = enterprisePropertiesPayload.mdefault.toMutableList().apply { - add(profileInfo) - } - } - } - return SyncRequest.Builder() - .enterprisePropertiesPayload(enterprisePropertiesPayload.build()).build() - } - - private fun createHardwareIdentifierPayloadRequest(context: Context): SyncRequest { - val builder = HardwareIdentifierPayload.Builder() - val telephonyManager = - context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - var imeid: Long = 0 - if (telephonyManager != null) { - //random imei - val randomIMEI = generateRandomIMEI() - imeid = if (TextUtils.isEmpty(randomIMEI) || !Pattern.compile("^[0-9]{15}$") - .matcher(randomIMEI).matches() - ) 0L else randomIMEI.toLong(10) or 0x1000000000000000L - if (imeid == 0L) { - var meid = "" - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - meid = telephonyManager.meid - } - if (!TextUtils.isEmpty(meid) && Pattern.compile("^[0-9a-fA-F]{14}$").matcher(meid) - .matches() - ) { - imeid = meid.toLong(16) or 0x1100000000000000L - if (imeid == 0L) { - if (context.packageManager.checkPermission( - "android.permission.READ_PRIVILEGED_PHONE_STATE", - "com.android.vending" - ) == PackageManager.PERMISSION_GRANTED - ) { - var serial = "" - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - serial = Build.getSerial() - } - if (TextUtils.isEmpty(serial) && serial != "unknown") { - try { - val serialShaByte = MessageDigest.getInstance("SHA1") - .digest(serial.toByteArray()) - imeid = - ((serialShaByte[0].toLong()) and 0xFFL) shl 0x30 or 0x1400000000000000L or (((serialShaByte[1].toLong()) and 0xFFL) shl 40) or (((serialShaByte[2].toLong()) and 0xFFL) shl 0x20) or (((serialShaByte[3].toLong()) and 0xFFL) shl 24) or (((serialShaByte[4].toLong()) and 0xFFL) shl 16) or (((serialShaByte[5].toLong()) and 0xFFL) shl 8) or ((serialShaByte[6].toLong()) and 0xFFL) - } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { - Log.w(TAG, "No support for sha1?") - } - } - } - } - } - } - builder.imeid(imeid) - } - return SyncRequest.Builder().HardwareIdentifierPayloadVALUE(builder.build()).build() - } - - private fun createHardwarePropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val HardwarePropertiesPayload_ = HardwarePropertiesPayload.Builder() - HardwarePropertiesPayload_.isLowRamDevice(deviceInfoCollect.isLowRamDevice) - .totalMem(deviceInfoCollect.totalMem) - .availableProcessors(deviceInfoCollect.availableProcessors) - .supportedAbis(deviceInfoCollect.supportedAbis).build() - return SyncRequest.Builder() - .HardwarePropertiesPayloadVALUE(HardwarePropertiesPayload_.build()).build() - } - - private fun createLocalePropertiesPayloadRequest(): SyncRequest { - val builder = LocalePropertiesPayload.Builder().b("GMT+08:00") - return SyncRequest.Builder().LocalePropertiesPayloadVALUE(builder.build()).build() - } - - private fun createPlayPartnerPropertiesPayloadRequest(): SyncRequest { - val builder = PlayPartnerPropertiesPayload.Builder() - builder.marketId("am-google").partnerIdMs("play-ms-android-google") - .partnerIdAd("play-ad-ms-android-google") - return SyncRequest.Builder().PlayPartnerPropertiesPayloadVALUE(builder.build()).build() - } - - private fun createPlayPropertiesPayload(context: Context): SyncRequest { - var version = 0 - try { - version = context.packageManager.getPackageInfo("com.android.vending", 0).versionCode - } catch (`packageManager$NameNotFoundException0`: PackageManager.NameNotFoundException) { - Log.w(TAG, "[DAS] Could not find our package", `packageManager$NameNotFoundException0`) - } - val playPropertiesPayload = PlayPropertiesPayload.Builder().playVersion(version).build() - return SyncRequest.Builder().PlayPropertiesPayloadVALUE(playPropertiesPayload).build() - } - - private fun createScreenPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val builder = ScreenPropertiesPayload.Builder() - builder.reqTouchScreen(deviceInfoCollect.reqTouchScreen) - .displaySizex(deviceInfoCollect.displaySizex) - .displaySizey(deviceInfoCollect.displaySizey) - .desityDeviceStablePoint(deviceInfoCollect.desityDeviceStablePoint) - .desityDeviceStable(deviceInfoCollect.desityDeviceStable) - return SyncRequest.Builder().ScreenPropertiesPayloadVALUE(builder.build()).build() - } - - private fun createSystemPropertiesPayloadRequest(deviceInfoCollect: DeviceInfoCollect): SyncRequest { - val SystemPropertiesPayload_ = SystemPropertiesPayload.Builder() - SystemPropertiesPayload_.fingerprint("google/sunfish/sunfish:13/TQ2A.230405.003/9719927:user/release-keys") - .sdkInt(Build.VERSION.SDK_INT.toLong()) - .previewSdkFingerprint(deviceInfoCollect.previewSdkFingerprint) - .buildCodeName(deviceInfoCollect.buildCodeName).oemkey1(deviceInfoCollect.oemkey1) - .reqGlEsVersion(deviceInfoCollect.reqGlEsVersion) - return SyncRequest.Builder().SystemPropertiesPayloadVALUE(SystemPropertiesPayload_.build()) - .build() - } - - private fun createGpuPayloadRequest(gpuInfos: List): SyncRequest { - var gpuInfos = gpuInfos - var gpuPayloads = emptyList() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - gpuInfos = gpuInfos.stream() - .filter { fetchedGlStrings: FetchedGlStrings -> !fetchedGlStrings.glRenderer!!.isEmpty() || !fetchedGlStrings.glVendor!!.isEmpty() || !fetchedGlStrings.glVersion!!.isEmpty() } - .collect(Collectors.toList()) - val maxVersion = gpuInfos.stream() - .max(Comparator.comparingInt { fetchedGlStrings: FetchedGlStrings -> fetchedGlStrings.contextClientVersion }) - .map { obj: FetchedGlStrings -> obj.contextClientVersion } - if (maxVersion.isPresent) { - gpuInfos = gpuInfos.stream() - .filter { fetchedGlStrings: FetchedGlStrings -> fetchedGlStrings.contextClientVersion == maxVersion.get() } - .collect(Collectors.toList()) - } - gpuPayloads = gpuInfos.stream().map { fetchedGlStrings: FetchedGlStrings -> - val gpuInfoWrapper_ = GpuInfoWrapper.Builder() - if (!TextUtils.isEmpty(fetchedGlStrings.glRenderer)) gpuInfoWrapper_.glRenderer( - fetchedGlStrings.glRenderer - ) - if (!TextUtils.isEmpty(fetchedGlStrings.glVendor)) gpuInfoWrapper_.glVendor( - fetchedGlStrings.glVendor - ) - if (!TextUtils.isEmpty(fetchedGlStrings.glVersion)) gpuInfoWrapper_.glVersion( - fetchedGlStrings.glVersion - ) - GpuPayload.Builder().gpuInfo(gpuInfoWrapper_.build()).build() - }.distinct().collect(Collectors.toList()) - } - - return SyncRequest.Builder().GpuPayloadVALUE( - if (gpuPayloads.isEmpty()) GpuPayload.Builder().build() else gpuPayloads[0] - ).build() - } - - private fun fetchGLInfo(): ArrayList? { - Log.d(TAG, "fetchGLInfo: ") - val eGL100 = EGLContext.getEGL() as EGL10 - val result = ArrayList() - val egl10Instance = if (eGL100 == null) null else EGL10Wrapper(eGL100) - if (eGL100 == null) { - Log.w(TAG, "Couldn't get EGL") - return null - } - val eglDisplay = eGL100.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY) - eGL100.eglInitialize(eglDisplay, IntArray(2)) - val numConfig = IntArray(1) - val configCount = - if (eGL100.eglGetConfigs(eglDisplay, null, 0, numConfig)) numConfig[0] else 0 - if (configCount <= 0) { - Log.w(TAG, "Couldn't get EGL config count") - return null - } - var configs: Array? = arrayOfNulls(configCount) - configs = if (eGL100.eglGetConfigs( - eglDisplay, - configs, - configCount, - IntArray(1) - ) - ) configs else null - if (configs == null) { - Log.w(TAG, "Couldn't get EGL configs") - return null - } - val arr_v1 = intArrayOf( - EGL10.EGL_WIDTH, - EGL10.EGL_PBUFFER_BIT, - EGL10.EGL_HEIGHT, - EGL10.EGL_PBUFFER_BIT, - EGL10.EGL_NONE - ) - for (index in 0 until configCount) { - if (egl10Instance!!.eglGetConfigAttrib( - eglDisplay, - configs[index], - EGL10.EGL_CONFIG_CAVEAT - ) != 0x3050 - && (egl10Instance.eglGetConfigAttrib( - eglDisplay, - configs[index], - EGL10.EGL_SURFACE_TYPE - ) and 1) != 0 - ) { - val attributeValue = egl10Instance.eglGetConfigAttrib( - eglDisplay, - configs[index], - EGL10.EGL_RENDERABLE_TYPE - ) - if ((attributeValue and 1) != 0) { - result.add( - buildGLStrings( - egl10Instance, - eglDisplay, - configs[index], - arr_v1, - null - ) - ) - } - - if ((attributeValue and 4) != 0) { - result.add( - buildGLStrings( - egl10Instance, - eglDisplay, - configs[index], - arr_v1, - intArrayOf(0x3098, EGL10.EGL_PIXMAP_BIT, EGL10.EGL_NONE) - ) - ) - } - } - } - egl10Instance!!.eglinstance.eglTerminate(eglDisplay) - return result - } - - private fun buildGLStrings( - egl10Tools: EGL10Wrapper?, - eglDisplay: EGLDisplay, - eglConfig: EGLConfig?, - arr_v: IntArray, - arr_v1: IntArray? - ): FetchedGlStrings? { - val eglContext = egl10Tools!!.eglinstance.eglCreateContext( - eglDisplay, - eglConfig, - EGL10.EGL_NO_CONTEXT, - arr_v1 - ) - if (eglContext !== EGL10.EGL_NO_CONTEXT) { - val eglSurface = - egl10Tools.eglinstance.eglCreatePbufferSurface(eglDisplay, eglConfig, arr_v) - if (eglSurface === EGL10.EGL_NO_SURFACE) { - egl10Tools.eglDestroyContext(eglDisplay, eglContext) - return null - } - egl10Tools.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) - val result = FetchedGlStrings(0, null, null, null, null) - val glExtensions = GLES10.glGetString(GLES10.GL_EXTENSIONS) - if (!TextUtils.isEmpty(glExtensions)) { - result.glExtensions = - glExtensions.split(" ".toRegex()).dropLastWhile { it.isEmpty() } - .toTypedArray() - } - result.glRenderer = GLES10.glGetString(GLES10.GL_RENDERER) - result.glVendor = GLES10.glGetString(GLES10.GL_VENDOR) - result.glVersion = GLES10.glGetString(GLES10.GL_VERSION) - if (result.glExtensions != null) { - egl10Tools.eglMakeCurrent( - eglDisplay, - EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_SURFACE, - EGL10.EGL_NO_CONTEXT - ) - egl10Tools.eglinstance.eglDestroySurface(eglDisplay, eglSurface) - egl10Tools.eglDestroyContext(eglDisplay, eglContext) - return result - } - - val stringBuilder = StringBuilder() - - if (result.glExtensions == null) { - stringBuilder.append(" glExtensions") - } - throw IllegalStateException("Missing required properties:$stringBuilder") - } - return null - } - - fun calculateSHA(data: ByteArray, algorithm: String?): String? { - val messageDigest0: MessageDigest - try { - messageDigest0 = MessageDigest.getInstance(algorithm) - } catch (noSuchAlgorithmException0: NoSuchAlgorithmException) { - Log.w(TAG, "[DC] No support for %s?", noSuchAlgorithmException0) - return null - } - - messageDigest0.update(data, 0, data.size) - return Base64.encodeToString(messageDigest0.digest(), 11) - } - - fun getSystemProperty(key: String?, defaultValue: String?): String? { - var value = defaultValue - try { - @SuppressLint("PrivateApi") val systemPropertiesClass = - Class.forName("android.os.SystemProperties") - val getMethod = - systemPropertiesClass.getMethod("get", String::class.java, String::class.java) - value = getMethod.invoke(null, key, defaultValue) as String - } catch (e: Exception) { - Log.w(TAG, "Unable to retrieve system property", e) - } - return value - } - - fun calculatePoint(point: Point, v: Int): Int { - val f = point.x.toFloat() - val v1 = ((point.y.toFloat()) * (160.0f / (v.toFloat()))).toInt() - if (v1 < 470) { - return 17 - } - - val v2 = (f * (160.0f / (v.toFloat()))).toInt() - if (v1 >= 960 && v2 >= 720) { - return if (v1 * 3 / 5 < v2 - 1) 20 else 4 - } - - val v3 = if (v1 < 640 || v2 < 480) 2 else 3 - return if (v1 * 3 / 5 < v2 - 1) v3 or 16 else v3 - } - - class EGL10Wrapper internal constructor(val eglinstance: EGL10) { - fun eglGetConfigAttrib(eglDisplay: EGLDisplay?, eglConfig: EGLConfig?, v: Int): Int { - val value = IntArray(1) - eglinstance.eglGetConfigAttrib(eglDisplay, eglConfig, v, value) - eglinstance.eglTerminate(eglDisplay) - return value[0] - } - - fun eglDestroyContext(eglDisplay: EGLDisplay?, eglContext: EGLContext?) { - eglinstance.eglDestroyContext(eglDisplay, eglContext) - } - - fun eglMakeCurrent( - eglDisplay: EGLDisplay?, - draw: EGLSurface?, - read: EGLSurface?, - eglContext: EGLContext? - ) { - eglinstance.eglMakeCurrent(eglDisplay, draw, read, eglContext) - } - } - - - class FetchedGlStrings( - var contextClientVersion: Int, - var glExtensions: Array?, - var glRenderer: String?, - var glVendor: String?, - var glVersion: String? - ) -} 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 index c7b194bcd6..4b60d02cdf 100644 --- 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 @@ -4,403 +4,272 @@ import android.accounts.Account import android.accounts.AccountManager import android.annotation.SuppressLint import android.content.Context -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.text.TextUtils +import android.database.Cursor import android.util.Base64 -import android.util.Log import androidx.collection.arrayMapOf -import com.android.vending.ExperimentAndConfigs.readExperimentsFlag -import com.android.vending.ExperimentAndConfigs.toByteArray -import com.google.android.phonesky.header.PayloadsProtoStore.readCache -import okio.ByteString.Companion.encodeUtf8 -import org.microg.vending.billing.CheckinServiceClient.getConsistencyToken -import org.microg.vending.billing.GServices.getString +import com.android.vending.AndroidVersionMeta +import com.android.vending.DeviceMeta +import com.android.vending.EncodedTriple +import com.android.vending.EncodedTripleWrapper +import com.android.vending.IntWrapper +import com.android.vending.LicenseRequestHeader +import com.android.vending.Locality +import com.android.vending.LocalityWrapper +import com.android.vending.RequestLanguagePackage +import com.android.vending.StringWrapper +import com.android.vending.Timestamp +import com.android.vending.TimestampContainer +import com.android.vending.TimestampContainer1 +import com.android.vending.TimestampContainer1Wrapper +import com.android.vending.TimestampContainer2 +import com.android.vending.TimestampStringWrapper +import com.android.vending.TimestampWrapper +import com.android.vending.UnknownByte12 +import com.android.vending.UserAgent +import com.android.vending.Util +import com.android.vending.Uuid +import okio.ByteString +import org.microg.gms.profile.Build +import org.microg.gms.settings.SettingsContract import java.io.ByteArrayOutputStream import java.io.DataOutputStream -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.io.IOException +import java.io.InputStream import java.io.OutputStream import java.net.HttpURLConnection import java.net.URL +import java.util.UUID import java.util.zip.GZIPOutputStream -object PhoneskyHeaderValue { - var TAG: String = PhoneskyHeaderValue::class.java.simpleName - private const val PHONESKY_HEADER_FILE = "finsky/shared/phonesky_header_valuestore.pb" - @SuppressLint("HardwareIds") - fun init(applicationContext: Context): PhoneskyValueStore? { - try { - val initiated = PhoneskyValue.Builder() +class GoogleApiRequest( + var url: String, + var method: String, + private val user: Account, + var context: Context, + private val externalxpsrh: RequestLanguagePackage? +) { + var content: ByteArray? = null + var timeout: Int = 3000 + var headerMap: MutableMap = arrayMapOf() + private val tokenType = "oauth2:https://www.googleapis.com/auth/googleplay" + var gzip: Boolean = false - initiated.ConsistencyTokenWrapperValue( - ConsistencyTokenWrapper.Builder() - .ConsistencyToken(getConsistencyToken(applicationContext)) - .unknowTokenf("").build() - ) - initiated.baseDeviceInfoValue( - BaseDeviceInfo.Builder() - .device(Build.DEVICE) - .hardware(Build.HARDWARE) - .model(Build.MODEL) - .product(Build.PRODUCT) - .androidId( - getString(applicationContext.contentResolver, "android_id", "")!! - .toLong() - ) - .gpVersion( - Uri.encode( - applicationContext.packageManager.getApplicationInfo( - applicationContext.packageName, - PackageManager.GET_META_DATA - ).metaData.getString("GpVersion") - ).replace("(", "%28").replace(")", "%29") - ) - .fingerPrint(Build.FINGERPRINT).build() - ) + init { + headerMap["User-Agent"] = buildUserAgent() + } - initiated.deviceBuildInfoValue( - DeviceBuildInfo.Builder() - .buildInfo( - BuildInfo.Builder() - .sdkInt(Build.VERSION.SDK_INT) - .id(Build.ID) - .release(Build.VERSION.RELEASE) - .constInte(84122130).build() - ) - .marketClientId("am-google") //"market_client_id" - .unknowBooleD(true) //getResources(xxx)^1 - .build() - ) + @SuppressLint("DefaultLocale") + private fun buildUserAgent(): String { + val versionName = "41.2.21-31" + val versionCode = "84122130" + val apiLevel = Build.VERSION.SDK_INT + val device = Build.DEVICE + val hardware = Build.HARDWARE + val product = Build.PRODUCT + val release = Build.VERSION.RELEASE + val model = Build.MODEL + val buildId = Build.ID + var supportedAbis: String? = null + supportedAbis = + if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + java.lang.String.join(";", *Build.SUPPORTED_ABIS) + } else { + Build.CPU_ABI + ";" + Build.CPU_ABI2 + } - val result = PhoneskyValueStore.Builder() - val phoneskyValueMutableMap = result.values.toMutableMap() - phoneskyValueMutableMap[""] = initiated.build() - result.values = phoneskyValueMutableMap - return result.build() - } catch (e: Exception) { - Log.w(TAG, "PhoneskyHeaderValue.Init", e) - } - return null + return String.format( + "Android-Finsky/%s [0] [PR] 636997666 (api=%d,versionCode=%s,sdk=%d,device=%s,hardware=%s,product=%s,platformVersionRelease=%s,model=%s,buildId=%s,isWideScreen=%d,supportedAbis=%s)", + versionName,apiLevel,versionCode,apiLevel, + device, + hardware, + product, + release, + model, + buildId, + 0, + supportedAbis + ) } - fun getPhoneskyHeader(context: Context, account: Account) { - var request = GoogleApiRequest( - "https://play-fe.googleapis.com/fdfe/toc?nocache_isui=true", - "GET", - account, - context, - buildataFdfe(context) - ) - val result = request.sendRequest(null) - val tocToken = TocToken.Builder() - .token(result!!.fdfeApiResponseValue?.tocApi?.tocTokenValue?.encodeUtf8()) + private fun makeTimestamp(millis: Long): Timestamp? { + return Timestamp.Builder() + .seconds(millis / 1000) + .nanos(Math.floorMod(millis, 1000) * 1000000) .build() - writePhonesky(context, "", object : WritePhoneskyCallback { - override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { - return data.tocTokenValue(tocToken).build() - } - - }) + } - val firstSyncData = SyncReqWrapper.Builder().mvalue( - listOf( - SyncRequest.Builder() - .UnknowTypeFirstSyncValue(UnknowTypeFirstSync.Builder().build()).build() + private fun getXHeaders(): String { + val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"; + var millis = System.currentTimeMillis() + val timestamp = TimestampContainer.Builder() + .container2( + TimestampContainer2.Builder() + .wrapper( + TimestampWrapper.Builder() + .timestamp(makeTimestamp(millis)).build() + ) + .timestamp(makeTimestamp(millis)) + .build() ) - ).build() - request = GoogleApiRequest( - "https://play-fe.googleapis.com/fdfe/sync", - "POST", - account, - context, - buildataFdfe(context) - ) - request.content = firstSyncData.encode() - val resultSyncFirst = request.sendRequest(null) - writePhonesky(context, "", object : WritePhoneskyCallback { - override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { - return data.sysncTokenValue( - resultSyncFirst!!.fdfeApiResponseValue?.syncResult?.syncTokenValue - ).build() - } - }) - - val requestData = readCache(context) - request = GoogleApiRequest( - "https://play-fe.googleapis.com/fdfe/sync?nocache_qos=lt", - "POST", - account, + val androidId = SettingsContract.getSettings( context, - buildataFdfe(context) + SettingsContract.CheckIn.getContentUri(context), + arrayOf(SettingsContract.CheckIn.ANDROID_ID) + ) { cursor: Cursor -> cursor.getLong(0) } + + millis = System.currentTimeMillis() + timestamp + .container1Wrapper( + TimestampContainer1Wrapper.Builder() + .androidId(androidId.toString()) + .container( + TimestampContainer1.Builder() + .timestamp(millis.toString() + "000") + .wrapper(makeTimestamp(millis)) + .build() + ) + .build() + ) + val encodedTimestamps = String( + Base64.encode( + Util.encodeGzip(timestamp.build().encode()), + Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING + ) ) - request.content = requestData!!.encode() - val resultSync = request.sendRequest(null) - - writePhonesky(context, "", object : WritePhoneskyCallback { - override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { - return data.sysncTokenValue( - resultSync!!.fdfeApiResponseValue?.syncResult?.syncTokenValue - ).build() - } - }) - - writePhonesky(context, account.name, object : WritePhoneskyCallback { - override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { - return data.experimentWrapperValue( - ExperimentWrapper.Builder().experServerTokenValue( - getExperimentTokenFor(context, account) - ).build() - ).build() - } - }) - - writePhonesky(context, "", object : WritePhoneskyCallback { - override fun modify(data: PhoneskyValue.Builder): PhoneskyValue { - return data.experimentWrapperValue( - ExperimentWrapper.Builder().experServerTokenValue( - getExperimentTokenFor(context, null) - ).build() - ).build() - } - }) - } - - private fun getExperimentTokenFor(context: Context, account: Account?): ExperServerToken { - val dataRegular = readExperimentsFlag( - context, - "com.google.android.finsky.regular", - if (account == null) "" else account.name + val locality = Locality.Builder() + .unknown1(1) + .unknown2(2) + .countryCode("") + .region( + TimestampStringWrapper.Builder() + .string("") + .timestamp(makeTimestamp(System.currentTimeMillis())).build() + ) + .country( + TimestampStringWrapper.Builder() + .string("") + .timestamp(makeTimestamp(System.currentTimeMillis())).build() + ) + .unknown3(0) + .build() + val encodedLocality = String( + Base64.encode(locality.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) ) - val dataStable = readExperimentsFlag(context, "com.google.android.finsky.stable", "") - val result = ExperServerToken.Builder() - if (dataRegular != null && !TextUtils.isEmpty(dataRegular.serverToken)) { - result.regularServerToken(dataRegular.serverToken) - } - if (dataStable != null && !TextUtils.isEmpty(dataStable.serverToken)) { - result.stableServerToken(dataStable.serverToken) - } - return result.build() - } - - //build base X-PS-RH for /fdfe/* - fun buildataFdfe(context: Context): PhoneskyValue { - return PhoneskyValue.Builder() - .unknowFieldk(PhoneskyUnknowFieldK.Builder().mvalue(5).build()) - .baseDeviceInfoValue( - BaseDeviceInfo.Builder().androidId( - getString(context.contentResolver, "android_id", "")!! - .toLong() + val header = LicenseRequestHeader.Builder() + .encodedTimestamps(StringWrapper.Builder().string(encodedTimestamps).build()) + .triple( + EncodedTripleWrapper.Builder().triple( + EncodedTriple.Builder() + .encoded1("") + .encoded2("") + .empty("") + .build() ).build() ) - .unknowDeviceIdValue( - UnknowDeviceId.Builder().uuid("00000000-0000-0000-0000-000000000000").type(1) + .locality(LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build()) + .unknown(IntWrapper.Builder().integer(5).build()) + .empty("") + .languages(externalxpsrh) + .deviceMeta( + DeviceMeta.Builder() + .android( + AndroidVersionMeta.Builder() + .androidSdk(org.microg.gms.profile.Build.VERSION.SDK_INT) + .buildNumber(org.microg.gms.profile.Build.ID) + .androidVersion(org.microg.gms.profile.Build.VERSION.RELEASE) + .unknown(0) + .build() + ) + .unknown1(UnknownByte12.Builder().bytes(ByteString.EMPTY).build()) + .unknown2(1) .build() - ).build() - } - - private fun writePhonesky(context: Context, key: String, callback: WritePhoneskyCallback) { - val file = File(context.filesDir, PHONESKY_HEADER_FILE) - var existData: PhoneskyValueStore.Builder? = null - if (file.exists()) { - val input = FileInputStream(file) - existData = PhoneskyValueStore.ADAPTER.decode(input).newBuilder() - input.close() - } else { - if (file.parentFile?.exists() == true || file.parentFile?.mkdirs() == true) { - if (file.createNewFile()) { - existData = init(context)?.newBuilder() - } else { - throw RuntimeException("create file failed") - } - } - } - if (existData != null) { - val phoneskyValueMap = existData.values.toMutableMap() - if (existData.values.containsKey(key)) { - - val modifed = callback.modify(if (existData.values[key] != null) { - existData.values[key]!!.newBuilder() - } else { - PhoneskyValue.Builder() - }) - phoneskyValueMap[key] = modifed - } else { - val modifed = callback.modify(PhoneskyValue.Builder()) - phoneskyValueMap[key] = modifed - } - existData.values = phoneskyValueMap - val outputStream = FileOutputStream(file) - outputStream.write(existData.build().encode()) - outputStream.close() - } + ) + .userAgent( + UserAgent.Builder() + .deviceName(org.microg.gms.profile.Build.DEVICE) + .deviceHardware(org.microg.gms.profile.Build.HARDWARE) + .deviceModelName(org.microg.gms.profile.Build.MODEL) + .finskyVersion(FINSKY_VERSION) + .deviceProductName(org.microg.gms.profile.Build.MODEL) + .androidId(androidId) // must not be 0 + .buildFingerprint(org.microg.gms.profile.Build.FINGERPRINT) + .build() + ) + .uuid( + Uuid.Builder() + .uuid(UUID.randomUUID().toString()) + .unknown(2) + .build() + ) + .build().encode() + return String(Base64.encode(Util.encodeGzip(header), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)) } - interface WritePhoneskyCallback { - fun modify(data: PhoneskyValue.Builder): PhoneskyValue + private fun getHeaders(): Map { + headerMap["X-PS-RH"] = getXHeaders() + headerMap["Authorization"] = "Bearer " + AccountManager.get(context).getAuthToken( + user, tokenType, null, false, null, null + ).result.getString(AccountManager.KEY_AUTHTOKEN) + return this.headerMap } - class GoogleApiRequest( - var url: String, - var method: String, - private val user: Account, - var context: Context, - private val externalxpsrh: PhoneskyValue? - ) { - var content: ByteArray? = null - var timeout: Int = 3000 - var headerMap: MutableMap = arrayMapOf() - private val tokenType = "oauth2:https://www.googleapis.com/auth/googleplay" - var gzip: Boolean = false - - init { - headerMap["User-Agent"] = buildUserAgent() + 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]) } - - @SuppressLint("DefaultLocale") - private fun buildUserAgent(): String { - val versionName = "41.2.21-31" - val versionCode = "84122130" - val apiLevel = Build.VERSION.SDK_INT - val device = Build.DEVICE - val hardware = Build.HARDWARE - val product = Build.PRODUCT - val release = Build.VERSION.RELEASE - val model = Build.MODEL - val buildId = Build.ID - var supportedAbis: String? = null - supportedAbis = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - java.lang.String.join(";", *Build.SUPPORTED_ABIS) + 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 { - Build.CPU_ABI + ";" + Build.CPU_ABI2 + DataOutputStream(httpURLConnection.outputStream) } - return String.format( - "Android-Finsky/%s [0] [PR] 636997666 (api=%d,versionCode=%s,sdk=%d,device=%s,hardware=%s,product=%s,platformVersionRelease=%s,model=%s,buildId=%s,isWideScreen=%d,supportedAbis=%s)", - versionName,apiLevel,versionCode,apiLevel, - device, - hardware, - product, - release, - model, - buildId, - 0, - supportedAbis - ) - } - - fun addHeader(key: String, value: String) { - headerMap[key] = value - } - - fun getHeaders(): Map { - val phoneksyHeaderFile = File(context.filesDir, PHONESKY_HEADER_FILE) - var existData = PhoneskyValueStore.Builder().build() - if (phoneksyHeaderFile.exists()) { - val input = FileInputStream(phoneksyHeaderFile) - existData = PhoneskyValueStore.ADAPTER.decode(input) - input.close() - } - var xpsrh = PhoneskyValue.Builder().build() - if (existData.values.containsKey("")) { - xpsrh = existData.values[""]!! - } - if (existData.values.containsKey(user.name)) { - mergeProto(xpsrh, existData.values[user.name]) + dataOutputStream.write(content) + dataOutputStream.close() } - if (externalxpsrh != null) { - mergeProto(xpsrh, externalxpsrh) - } - headerMap["X-PS-RH"] = Base64.encodeToString( - gzip( - xpsrh!!.encode() - ), Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP - ) - headerMap["Authorization"] = "Bearer " + AccountManager.get(context).getAuthToken( - user, tokenType, null, false, null, null - ).result.getString(AccountManager.KEY_AUTHTOKEN) - return this.headerMap } - - fun mergeProto(data1: PhoneskyValue?, data2: PhoneskyValue?) { - for (data in PhoneskyValue::class.java.declaredFields) { - data.isAccessible = true - if (data[data2] != null && data[data1] == null) { - data[data1] = data[data2] - } - } + val responseCode = httpURLConnection.responseCode + if (responseCode == HttpURLConnection.HTTP_OK) { + val data = toByteArray(httpURLConnection.inputStream) + return GoogleApiResponse.ADAPTER.decode(data) } - 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 = toByteArray(httpURLConnection.inputStream) - return GoogleApiResponse.ADAPTER.decode(data) - } + return null + } - return null - } + private fun toByteArray(inputStream: InputStream): ByteArray { + val buffer = ByteArrayOutputStream() + var nRead: Int + val data = ByteArray(1024) - companion object { - fun gzip(arr_b: ByteArray?): ByteArray { - try { - ByteArrayOutputStream().use { byteArrayOutputStream -> - GZIPOutputStream(byteArrayOutputStream).use { gzipOutputStream -> - gzipOutputStream.write(arr_b) - gzipOutputStream.finish() - val arr_b1 = byteArrayOutputStream.toByteArray() - arr_b1[9] = 0 - return arr_b1 - } - } - } catch (iOException0: IOException) { - Log.w("Unexpected %s", arrayOf(iOException0).contentToString()) - return ByteArray(0) - } - } + while ((inputStream.read(data, 0, data.size).also { nRead = it }) != -1) { + buffer.write(data, 0, nRead) } + buffer.flush() + return buffer.toByteArray() } -} +} \ No newline at end of file diff --git a/vending-app/src/main/proto/ExperimentsAndConfigs.proto b/vending-app/src/main/proto/ExperimentsAndConfigs.proto deleted file mode 100644 index 142e60857b..0000000000 --- a/vending-app/src/main/proto/ExperimentsAndConfigs.proto +++ /dev/null @@ -1,167 +0,0 @@ - -option java_package = "com.google.android.finsky"; -option java_multiple_files = true; - -message experimentRequestData { - optional DeviceData deviceDataValue = 1; - repeated ExperimentsInfo experimentsInfo = 2; - optional bytes bytesTag = 3; - optional action actionType = 4; - optional int32 unknowFieldG = 5; - optional string expPkgName = 7; -} - -enum action { - UNSPECIFIED =0; - PERIODIC = 1; - ADAPTIVE = 2; - GCM_PUSH = 3; - APPLICATION_PUSH= 4; - APPLICATION_PUSH_RETRY=11; - NEW_USER= 5; - NEW_APPLICATION= 6; - NEW_REGISTER_VERSION= 7; - NEW_REGISTER_OTHER= 8; - MOBDOG= 9; - RETRY_AFTER= 10; - NEW_USER_SYNC= 12; - NEW_APPLICATION_SYNC= 13; - NEW_REGISTER_VERSION_SYNC= 14; - NEW_REGISTER_OTHER_SYNC= 15; - NEW_APP_PROPERTIES= 16; - GMSCORE_SAFEBOOT= 17; - CHECK_IN= 18; - FORCE_SYNC= 19; - FLAG_CORRUPTION= 20; - FLAG_OVERRIDE= 21; - MOBILE_UTILITIES= 22; -} - -message ExperimentsInfo { - optional ExperimentVersion experimentVersionValue = 1; - optional bytes unKnowBytesC = 2; - repeated ApplicationTag applicationTagValue =3; - optional bytes tokensTag = 4; -} - -message UnknowMsg { - optional int32 field1 = 1; -} - -message ApplicationTag { - optional int64 partitionId = 1; - optional bytes tag = 2; -} - -message ExperimentVersion { - optional string expPkgName = 1; - optional int64 version = 2; - optional ExperimentFlag experimentFlagValue = 3; - optional sfixed64 baselineCL = 4; - optional string pkgName = 5; -} - -message ExperimentFlag { - optional fixed64 flag = 1; -} - -message DeviceData { - optional int64 hasAccount = 2; - optional ExpDeviceInfoWrapper expDeviceInfoWrapperValue = 4; - optional bytes unknowEmptyE = 6; - optional bool unknowFlagf = 7; - optional DeviceDataEmptyA unknkowFieldG = 8; -} - -message DeviceDataEmptyA { - optional bytes unknowFieldB = 1; -} - -message ExpDeviceInfoWrapper{ - optional int32 unknowFieldb = 1; - optional ExpDeviceInfo expDeviceInfoValue = 2; -} - - -message ExpDeviceInfo { - optional int64 androidId = 1; - optional int32 sdkInt = 3; - optional string model = 4; - optional string product = 5; - optional string buildId = 6; - optional string buildDevice = 9; - optional string unknowEmpty = 10; - optional string locale = 11; - optional string country = 12; - optional string manufacturer = 13; - optional string fingerprint = 17; - repeated string supportAbis = 31; -} - - -message ExperimentResponseData { - repeated ExperimentsDataWrapper experiments =1; - optional bytes bytesTag = 2; - optional int64 servingVersion = 4; -} - -message ExperimentsDataWrapper { - optional ExperimentVersion experimentVersionValue = 1; - repeated ExpFlagsGroup expFlagsGroupValue =2; //c - optional bytes experimentToken = 3; - optional string serverToken = 4; - optional bytes tokensTag = 7; -} - -//message autv { -// optional experimentVersion experimentVersionValue = 1; -//} - -message ExpFlagsGroup { - optional ApplicationTag applicationTagValue = 1; - repeated ExpFlag expFlags =2; -} - -message ExpFlag { - optional string flagName = 1; - optional int64 longValue = 2; - optional bool boolValue = 3; - optional double doubleValue = 4; - optional string stringValue = 5; - optional ExtensionValue extensionValueValue = 6; - optional int32 valueType = 9; //enum -} - -message ExtensionValue { - optional bytes mvalue = 1; -} - -//----------------------------experimentsFlags protostore message---- -message ExperimentsFlagsProto { - optional string expeIntroduce = 2; //b - optional bytes experimentToken = 3; //c - optional string serverToken = 1; //d - optional bool unknowFlagB = 8; //g - optional int64 servingVersion = 9; //h - repeated FlagValueProto flagValues =4; //e -} - -message FlagValueProto { - oneof c { - fixed64 intVal = 1; - bool boolVal = 2; - double floatVal = 3; - string stringVal = 4; - bytes extensionVal = 5; - } - optional string name = 10; -} - -message ExperimentTokenStore { - repeated bytes experimentToken = 2; //c -} - - - - - diff --git a/vending-app/src/main/proto/LicenseRequest.proto b/vending-app/src/main/proto/LicenseRequest.proto index dc0b71339e..c46d5d2d44 100644 --- a/vending-app/src/main/proto/LicenseRequest.proto +++ b/vending-app/src/main/proto/LicenseRequest.proto @@ -9,6 +9,7 @@ message LicenseRequestHeader { optional LocalityWrapper locality = 11; optional IntWrapper unknown = 12; optional string empty = 14; + optional RequestLanguagePackage languages = 15; optional DeviceMeta deviceMeta = 20; optional UserAgent userAgent = 21; optional Uuid uuid = 27; @@ -36,6 +37,10 @@ message IntWrapper { optional uint32 integer = 1; } +message RequestLanguagePackage { + repeated string language = 1; +} + message DeviceMeta { optional AndroidVersionMeta android = 1; optional UnknownByte12 unknown1 = 2; diff --git a/vending-app/src/main/proto/PhoneskyHeaderValueStore.proto b/vending-app/src/main/proto/PhoneskyHeaderValueStore.proto deleted file mode 100644 index 62fbc3fefd..0000000000 --- a/vending-app/src/main/proto/PhoneskyHeaderValueStore.proto +++ /dev/null @@ -1,433 +0,0 @@ - -option java_package = "com.google.android.phonesky.header"; -option java_multiple_files = true; - -message PhoneskyValueStore{//aknj - map values = 2; -} - -message PhoneskyValue{ //ayie - optional SyncToken sysncTokenValue = 1; - optional ExperimentWrapper experimentWrapperValue = 10; - optional TocToken tocTokenValue = 11; //j - optional PhoneskyUnknowFieldK unknowFieldk = 12; - optional RequestLanguagePkg languages = 15; - optional ConsistencyTokenWrapper ConsistencyTokenWrapperValue = 16; //n - optional DeviceBuildInfo deviceBuildInfoValue = 20; //q - optional BaseDeviceInfo baseDeviceInfoValue = 21; //r - optional UnknowDeviceId unknowDeviceIdValue = 27; -} - -message UnknowDeviceId { - optional string uuid = 1; - optional int32 type = 2; //enum -} - -message BaseDeviceInfo { - optional string device = 1; - optional string hardware = 2; - optional string model = 3; - optional string gpVersion = 4; //e - optional string product = 5; - optional int64 androidId = 6; //f - optional string fingerPrint = 7; -} - -message DeviceBuildInfo { - optional BuildInfo buildInfo = 1; - optional string marketClientId = 2; //c - optional bool unknowBooleD = 3; //d -} - -message BuildInfo { - optional int32 sdkInt = 1; //b - optional string id = 2; //c - optional string release = 3; //d - optional int32 constInte = 4; //e -} - -message ConsistencyTokenWrapper { - optional string ConsistencyToken = 1; //b - optional string unknowTokenc = 2; //c - optional string unknowTokenf = 6; //f -} - -message RequestLanguagePkg { - repeated string language = 1; -} - -message PhoneskyUnknowFieldK { - optional int32 mvalue = 1; //enum -} - -message TocToken { - optional bytes token = 1; -} - -message ExperimentWrapper { - optional ExperServerToken experServerTokenValue = 1; -} - -message ExperServerToken { - optional string regularServerToken = 1; - optional string stableServerToken = 2; - optional bytes unknowField = 3; -} - -message SyncToken { - optional string mvalue = 1; -} - -enum PaurchaseType { - UNKNOWN_ITEM_TYPE = 0; - ANDROID_APP = 1; - ANDROID_APP_DEVELOPER = 2; - ANDROID_IN_APP_ITEM = 3; - DYNAMIC_ANDROID_IN_APP_ITEM = 4; - ANDROID_APP_SUBSCRIPTION = 5; - DYNAMIC_ANDROID_APP_SUBSCRIPTION = 6; - MOVIE = 7; - TV_SHOW = 8; - TV_SEASON = 9; - TV_EPISODE =10; - AUDIOBOOK =11; - AUDIOBOOK_SERIES =24; - EBOOK =12; - EBOOK_SERIES =13; - BOOK_AUTHOR =14; - ALBUM =15; - SONG =16; - MUSIC_ARTIST =17; - MAGAZINE =18; - MAGAZINE_ISSUE =19; - NEWSPAPER =20; - NEWS_ISSUE =21; - VOUCHER =22; - YOUTUBE_COMMERCE_ITEM =25; - LOYALTY_REWARD =26; - BOOK_SUBSCRIPTION =27; - LOYALTY_VOUCHER =28; - LOYALTY_PLAY_CREDIT =29; -} - -enum DeviceType { - NO_DEVICE = 0; - PHONE =1 ; - GTV =2 ; - TABLET =3 ; - TABLET_LARGE =4 ; - ANDROID_TV =5 ; - WEAR =6 ; - CHROMEBOOK =7 ; - ANDROID_AUTO =8 ; - HIGH_PERFORMANCE_EMULATOR =10 ; - ANDROID_XR =11 ; -} -//message awgc { -// optional feauture b = 1; -//} - -enum Feauture { - FEATURED_UNSPECIFIED =0; - FEATURED_TV_MOVIES =1; - FEATURED_ENTERTAINMENT_VIDEO =2; - FEATURED_EBOOK =3; - FEATURED_AUDIOBOOK =4; - FEATURED_BOOK_SERIES =5; - FEATURED_MUSIC =6; - FEATURED_PODCAST =7; - FEATURED_RADIO =8; - FEATURED_SHOPPING_PRODUCT =9; - FEATURED_FOOD_PRODUCT =10; - FEATURED_RECIPE =11; - FEATURED_FOOD_STORE =12; - FEATURED_GENERIC_CONTENT =13; -} - -message GoogleApiResponse { - optional FdfeApiResponse fdfeApiResponseValue = 1; - optional UnknowTypebbfe g= 5; - optional bytes unknowFieldBytes= 9; -} - -message UnknowTypebbfe { - optional int64 id=1; -} - -message FdfeApiResponse { - optional TocResponse tocApi = 6; - optional SplitResponse splitReqResult = 21; - optional SyncApiResp syncResult = 183; -} - -message TocResponse { -// optional bool o=11; - optional string tocTokenValue=22; //t -} - -message SplitResponse { - optional int32 b = 1; //unknow enum - optional PkgFetchInfo pkgList = 2; -} - -message PkgFetchInfo { - repeated SplitPkgInfo pkgDownlaodInfo = 15; -} - -message SplitPkgInfo { - optional string splitPkgName = 1; - optional int64 size = 2; - optional string checkSum = 4; - optional string downloadUrl1 = 5; - optional DownloadInfo slaveDownloadInfo = 8; - optional string mabyChecksum = 9; - optional string unknowPkgInfoF = 15; - optional DownloadInfo otherDownloadInfo = 16; -} - -message DownloadInfo { - optional int32 id = 1; //unknow enum - optional int64 size = 2; - optional string url = 3; -} - -//--------------------------request for /fdfe/sync-- - -message SyncReqWrapper { - repeated SyncRequest mvalue = 1; -} - -message SyncRequest { - oneof payload { - AccountAossiationPayload AccountAossiationPayloadVALUE = 7; - DeviceAccountsPaylaod DeviceAccountsPaylaodVALUE = 8; - CarrierPropertiesPayload CarrierPropertiesPayloadVALUE = 9; - DeviceCapabilitiesPayload DeviceCapabilitiesPayloadVALUE = 10; - DeviceInputPropertiesPayload DeviceInputPropertiesPayloadVALUE = 11; - DeviceModelPayload DeviceModelPayloadVALUE = 12; - EnterprisePropertiesPayload enterprisePropertiesPayload = 13; - HardwareIdentifierPayload HardwareIdentifierPayloadVALUE = 14; - HardwarePropertiesPayload HardwarePropertiesPayloadVALUE = 15; - LocalePropertiesPayload LocalePropertiesPayloadVALUE = 16; - NotificationRoutingInfoPayload NotificationRoutingInfoPayloadVALUE = 17; - PlayPartnerPropertiesPayload PlayPartnerPropertiesPayloadVALUE = 18; - PlayPropertiesPayload PlayPropertiesPayloadVALUE = 19; - ScreenPropertiesPayload ScreenPropertiesPayloadVALUE = 20; - SystemPropertiesPayload SystemPropertiesPayloadVALUE = 21; - GpuPayload GpuPayloadVALUE = 24; - UnknowTypeFirstSync UnknowTypeFirstSyncValue = 30; - } -} - -message AccountAossiationPayload { - optional AccountAssValue mvalue = 1; -} - -message AccountAssValue { - optional string mvalue = 1; -} - -message DeviceAccountsPaylaod { - repeated AccountAssValue mvalue = 1; -} - -message CarrierPropertiesPayload { - optional string simOperator = 1; - optional TelephonyStateWrapper telephonyStateValue = 2; -} - -message TelephonyStateWrapper { - optional TelephonyInfo mvalue = 1; -} - -message TelephonyInfo { - optional int64 subscriberId1 = 1; - optional string operatorName = 2; - optional string groupidLevel = 3; -// optional string e = 4; -// repeated string f = 5; - optional int32 simcardId = 6; - optional int32 CarrierIdFromSimMccMnc = 7; -} - -message DeviceCapabilitiesPayload { - repeated FeatureInfoProto featureInfos = 1; - repeated string systemSharedLibraryNames = 2; - repeated string locales = 3; - repeated string glExtensions = 4; - optional bool unknowFlag = 5; -} - -message DeviceInputPropertiesPayload { - optional int32 reqKeyboardType = 1; //unknow enum - optional bool reqInputFeatures1 = 2; - optional int32 reqNavigation = 3; //unknow enum -} - -message DeviceModelPayload { - optional string MANUFACTURER = 1; - optional string MODEL = 2; - optional string DEVICE = 3; - optional string PRODUCT = 4; - optional string BRAND = 5; -} - -message EnterprisePropertiesPayload { - optional ProfileInfo profileOwner = 1; - repeated ProfileInfo mdefault = 2; -} - -message ProfileInfo { - optional string pkgName = 1; - optional string pkgSHA1 = 2; - optional string pkgSHA256 = 3; - optional MangedScope policyTypeValue = 4; //unknow enum -} - -enum MangedScope { - UNKNOWN_MANAGED_SCOPE = 0; - MANAGED_DEVICES = 1; - MANAGED_PROFILES = 2; - MANAGED_AVENGER = 3; - LEGACY_DEVICE_ADMINS = 4; -} - -message HardwareIdentifierPayload { - optional fixed64 imeid = 1; -} - -message HardwarePropertiesPayload { - optional bool isLowRamDevice = 1; - optional int64 totalMem = 2; - optional int32 availableProcessors = 3; - repeated string supportedAbis = 4; -} - -message LocalePropertiesPayload { - optional string b = 1; -} - -message NotificationRoutingInfoPayload { - optional string locale = 1; -} - -message PlayPartnerPropertiesPayload { - optional string marketId = 1; - optional string partnerIdMs = 2; - optional string partnerIdAd = 3; -} - -message PlayPropertiesPayload { - optional int32 playVersion = 2; -} - -message ScreenPropertiesPayload { - optional int32 reqTouchScreen = 1; //unknow enum - optional int32 displaySizex = 2; - optional int32 displaySizey = 3; - optional int32 desityDeviceStablePoint = 4; //unknow enum - optional int32 desityDeviceStable = 5; -} - -message SystemPropertiesPayload { - optional string fingerprint = 1; - optional int64 sdkInt = 2; - optional string previewSdkFingerprint = 3; - optional string buildCodeName = 4; - optional string oemkey1 = 5; - optional int32 reqGlEsVersion = 6; -} - -message GpuPayload { - optional GpuInfoWrapper gpuInfo = 1; -} - -message GpuInfoWrapper { - optional string glRenderer = 1; - optional string glVendor = 2; - optional string glVersion = 3; -} - - - -message UnknowTypeFirstSync { -} - -//-----------------response for fdfe/sync -message SyncApiResp { - repeated SyncApiRespEmptyA unknowFieldA=1; - optional SyncToken syncTokenValue=2; -// repeated string c=3; -} - -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; -} - -//----------------------proto for payloads protostroe------ - -message DeviceInfoCollect { - optional int32 reqTouchScreen = 1; //unknow enum - optional int32 reqKeyboardType = 2; //unknow enum - optional int32 reqNavigation = 3; //unknow enum - optional int32 desityDeviceStablePoint = 4; //unknow enum - optional bool reqInputFeatures1 = 5; - optional bool reqInputFeatures2 = 6; - optional int32 desityDeviceStable = 7; //DENSITY_DEVICE_STABLE - optional int32 reqGlEsVersion = 8; - repeated string systemSharedLibraryNames = 9; - repeated string featureNames = 10; - repeated string supportedAbis = 11; - optional int32 displaySizex = 12; - optional int32 displaySizey = 13; - repeated string locales = 14; - repeated string glExtensions = 15; - optional int32 smallestScreenWidthDp = 18; - optional bool isLowRamDevice = 19; - optional int64 totalMem = 20; - optional int32 availableProcessors = 21; - repeated FeatureInfoProto featureInfos = 26; - optional int32 screenLayout = 27; //unknow enum - optional string oemkey1 = 29; - optional string buildCodeName = 30; - optional string previewSdkFingerprint = 31; -} - -message FeatureInfoProto { - optional string name = 1; - optional int32 version = 2; -} - -message ProfileInfoTemp { - optional string packageName = 1; - optional string pkgSHA1 = 2; - optional string pkgSHA256 = 3; - optional PolicyType policyTypeValue = 4; //unknow enum -} - -enum PolicyType { - UNKNOW = 0; - MANAGED_DEVICE = 1; - MANAGED_PROFILE = 2; - LEGACY_DEVICE_ADMIN = 3; -} - diff --git a/vending-app/src/main/proto/SplitInstall.proto b/vending-app/src/main/proto/SplitInstall.proto new file mode 100644 index 0000000000..0308b44e43 --- /dev/null +++ b/vending-app/src/main/proto/SplitInstall.proto @@ -0,0 +1,88 @@ + +option java_package = "com.google.android.phonesky.header"; +option java_multiple_files = true; + +message GoogleApiResponse { + optional FdfeApiResponse fdfeApiResponseValue = 1; + optional UnknowTypebbfe g= 5; + optional bytes unknowFieldBytes= 9; +} + +message UnknowTypebbfe { + optional int64 id=1; +} + +message FdfeApiResponse { + optional TocResponse tocApi = 6; + optional SplitResponse splitReqResult = 21; + optional SyncApiResp syncResult = 183; +} + +message TocResponse { +// optional bool o=11; + optional string tocTokenValue=22; //t +} + +message SplitResponse { + optional int32 b = 1; //unknow enum + optional PkgFetchInfo pkgList = 2; +} + +message PkgFetchInfo { + repeated SplitPkgInfo pkgDownlaodInfo = 15; +} + +message SplitPkgInfo { + optional string splitPkgName = 1; + optional int64 size = 2; + optional string checkSum = 4; + optional string downloadUrl1 = 5; + optional DownloadInfo slaveDownloadInfo = 8; + optional string mabyChecksum = 9; + optional string unknowPkgInfoF = 15; + optional DownloadInfo otherDownloadInfo = 16; +} + +message DownloadInfo { + optional int32 id = 1; //unknow enum + 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; +} + From e8a0058c92318266bcbb8e3f9e8fec973aaf2cff Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Thu, 22 Aug 2024 18:49:26 +0800 Subject: [PATCH 05/11] Optimizing the code --- .../splitinstallservice/ExampleUnitTest.java | 17 -- vending-app/src/main/AndroidManifest.xml | 5 - .../licensing/LicenseRequestHeaders.kt | 9 +- .../org/microg/vending/billing/Constants.kt | 4 +- .../SplitInstallServiceImpl.kt | 87 +++---- .../phonesky/header/PhoneskyHeaderValue.kt | 228 +++--------------- 6 files changed, 75 insertions(+), 275 deletions(-) delete mode 100644 play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java diff --git a/play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java b/play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java deleted file mode 100644 index 274bc2af20..0000000000 --- a/play-services-nearby/core/src/test/java/com/google/android/finsky/splitinstallservice/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.google.android.finsky.splitinstallservice; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index abf3a615ae..35bb3cb6b5 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -57,10 +57,6 @@ android:roundIcon="@mipmap/ic_app" android:label="@string/app_name"> - - @@ -98,7 +94,6 @@ - diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt index 157d40efc3..0ae45beb1e 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt @@ -35,7 +35,7 @@ private const val TAG = "FakeLicenseRequest" private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" -internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map { +internal fun getDefaultLicenseRequestHeaderBuilder(androidId: Long) : LicenseRequestHeader.Builder { var millis = System.currentTimeMillis() val timestamp = TimestampContainer.Builder() .container2( @@ -79,7 +79,7 @@ internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map { + val header = getDefaultLicenseRequestHeaderBuilder(androidId).build().encode() val xPsRh = String(Base64.encode(header.encodeGzip(), BASE64_FLAGS)) Log.v(TAG, "X-PS-RH: $xPsRh") diff --git a/vending-app/src/main/java/org/microg/vending/billing/Constants.kt b/vending-app/src/main/java/org/microg/vending/billing/Constants.kt index 4659a644a5..93af4deb03 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/Constants.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/Constants.kt @@ -14,6 +14,4 @@ const val VENDING_PACKAGE_NAME = "com.android.vending" // TODO: Replace key name const val KEY_IAP_SHEET_UI_PARAM = "key_iap_sheet_ui_param" const val DEFAULT_ACCOUNT_TYPE = "com.google" -const val ADD_PAYMENT_METHOD_URL = "https://play.google.com/store/paymentmethods" -const val FINSKY_REGULAR = "com.google.android.finsky.regular" -const val FINSKY_STABLE = "com.google.android.finsky.stable" \ No newline at end of file +const val ADD_PAYMENT_METHOD_URL = "https://play.google.com/store/paymentmethods" \ No newline at end of file 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 index 084af400e4..8798823f85 100644 --- 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 @@ -15,17 +15,17 @@ import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.Session -import android.content.pm.PackageManager 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.android.vending.RequestLanguagePackage 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 @@ -33,11 +33,11 @@ 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.nio.file.Files -import java.nio.file.Paths import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue import kotlin.concurrent.thread @@ -131,7 +131,6 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi Log.i(TAG, "Complete install for app update not implemented") } - @RequiresApi(api = Build.VERSION_CODES.N) override fun languageSplitInstall( pkg: String, splits: List, @@ -151,7 +150,6 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi Log.i(TAG, "Language split uninstallation requested but app not found, package: %s$pkg") } - @SuppressLint("StringFormatMatches") private fun trySplitInstall(pkg: String, splits: List, isLanguageSplit: Boolean) { Log.d(TAG, "trySplitInstall: $splits") val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -193,17 +191,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } val packageManager = context.packageManager - var versionCode: Long = 0 - try { - val packageInfo = packageManager.getPackageInfo(packageName, 0) - versionCode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - packageInfo.longVersionCode // For API level 28 and above - } else { - packageInfo.versionCode.toLong() // For API level 27 and below - } - } catch (e: PackageManager.NameNotFoundException) { - Log.e("SplitInstallServiceImpl", "Error getting package info", e) - } + 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()){ @@ -232,7 +220,6 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } } - @RequiresApi(Build.VERSION_CODES.O) private fun downloadSplitPackage(downloadUrls: ArrayList>) : Boolean{ Log.d(TAG, "downloadSplitPackage downloadUrl:$downloadUrls") var stat = true @@ -244,7 +231,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi connection.requestMethod = "GET" if (connection.responseCode == HttpURLConnection.HTTP_OK) { BufferedInputStream(connection.inputStream).use { inputstream -> - BufferedOutputStream(Files.newOutputStream(Paths.get(tempFilePath.toString(),downloadUrl[0]))).use { outputstream -> + BufferedOutputStream(FileOutputStream(File(tempFilePath.toString(),downloadUrl[0]))).use { outputstream -> inputstream.copyTo(outputstream) } } @@ -256,7 +243,6 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi return stat } - @RequiresApi(Build.VERSION_CODES.O) private fun installSplitPackage( downloadUrl: ArrayList>, packageName: String, @@ -297,10 +283,13 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi try { downloadUrl.forEach { item -> val pkgPath = File(tempFilePath.toString(),item[0]) - session.openWrite(item[0], 0, -1).use { outputstream -> - Files.newInputStream(pkgPath.toPath()).copyTo(outputstream) - session.fsync(outputstream) + session.openWrite(item[0], 0, -1).use { outputStream -> + FileInputStream(pkgPath).use { inputStream -> + inputStream.copyTo(outputStream) + } + session.fsync(outputStream) } + totalDownloaded += pkgPath.length() pkgPath.delete() } @@ -357,21 +346,20 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi val googleApiRequest = GoogleApiRequest( requestUrl.toString(), "GET", accounts[0], context, - RequestLanguagePackage.Builder().language(langName.filterNotNull()).build() + langName.filterNotNull() ) val response = googleApiRequest.sendRequest(null) val pkgs = response?.fdfeApiResponseValue?.splitReqResult?.pkgList?.pkgDownlaodInfo - Log.w(TAG, "response?.fdfeApiResponseValue?.splitReqResult?.pkgList?.pkgDownlaodInfo:" + pkgs); if (pkgs != null) { for (item in pkgs) { for (lang in langName) { - if (("config.$lang") == item.splitPkgName) { + if (TextUtils.equals("config.$lang", item.splitPkgName)) { downloadUrls.add(arrayOf(lang!!, item.downloadUrl1!!)) } } Log.d(TAG, "requestSplitsPackage: $splitName") for (split in splitName) { - if (split != null && split == item.splitPkgName) { + if (split != null && TextUtils.equals(split, item.splitPkgName)) { downloadUrls.add(arrayOf(split, item.downloadUrl1!!)) } } @@ -397,7 +385,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } } if(taskQueue.size <= 1){ - NotificationManagerCompat.from(context).cancel(1) + NotificationManagerCompat.from(context).cancel(NOTIFY_ID) sendCompleteBroad(context, intent) } } @@ -409,15 +397,14 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val intent0 = - intent.extras!![Intent.EXTRA_INTENT] as Intent? - intent0!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - ContextCompat.startActivity(context, intent0, null) + 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(1) + NotificationManagerCompat.from(context).cancel(NOTIFY_ID) val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) Log.d("InstallResultReceiver", errorMsg ?: "") Log.w(TAG, "onReceive: install fail") @@ -425,26 +412,28 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi } } catch (e: Exception) { taskQueue.clear() - NotificationManagerCompat.from(context).cancel(1) + NotificationManagerCompat.from(context).cancel(NOTIFY_ID) Log.w(TAG, "Error handling install result", e) } } - private fun sendCompleteBroad(context: Context, intent: Intent) { - Log.d(TAG, "sendCompleteBroad: $intent") - val extra = Bundle() - extra.putInt("status", 5) - extra.putLong("total_bytes_to_download", intent.getLongExtra("bytes_downloaded",0)) - extra.putString("languages", intent.getStringExtra("language")) - extra.putInt("error_code", 0) - extra.putInt("session_id", 0) - extra.putLong("bytes_downloaded", intent.getLongExtra("bytes_downloaded",0)) - val intent = Intent("com.google.android.play.core.splitinstall.receiver.SplitInstallUpdateIntentService") - intent.setPackage(intent.getStringExtra("pkg")) - intent.putExtra("session_state", extra) - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) - context.sendBroadcast(intent) + 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) } } 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 index 4b60d02cdf..b92df2d5ed 100644 --- 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 @@ -2,116 +2,45 @@ package com.google.android.phonesky.header import android.accounts.Account import android.accounts.AccountManager -import android.annotation.SuppressLint import android.content.Context import android.database.Cursor import android.util.Base64 -import androidx.collection.arrayMapOf -import com.android.vending.AndroidVersionMeta -import com.android.vending.DeviceMeta -import com.android.vending.EncodedTriple -import com.android.vending.EncodedTripleWrapper -import com.android.vending.IntWrapper -import com.android.vending.LicenseRequestHeader -import com.android.vending.Locality -import com.android.vending.LocalityWrapper +import android.util.Log import com.android.vending.RequestLanguagePackage -import com.android.vending.StringWrapper -import com.android.vending.Timestamp -import com.android.vending.TimestampContainer -import com.android.vending.TimestampContainer1 -import com.android.vending.TimestampContainer1Wrapper -import com.android.vending.TimestampContainer2 -import com.android.vending.TimestampStringWrapper -import com.android.vending.TimestampWrapper -import com.android.vending.UnknownByte12 -import com.android.vending.UserAgent -import com.android.vending.Util -import com.android.vending.Uuid -import okio.ByteString -import org.microg.gms.profile.Build +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.ByteArrayOutputStream import java.io.DataOutputStream -import java.io.InputStream import java.io.OutputStream import java.net.HttpURLConnection import java.net.URL -import java.util.UUID import java.util.zip.GZIPOutputStream +private const val TAG = "GoogleApiRequest" class GoogleApiRequest( - var url: String, - var method: String, - private val user: Account, - var context: Context, - private val externalxpsrh: RequestLanguagePackage? + private var url: String, + private var method: String, + private val account: Account, + private var context: Context, + private val requestLanguagePackage: List ) { - var content: ByteArray? = null - var timeout: Int = 3000 - var headerMap: MutableMap = arrayMapOf() - private val tokenType = "oauth2:https://www.googleapis.com/auth/googleplay" - var gzip: Boolean = false + private var content: ByteArray? = null + private var timeout: Int = 3000 + private var gzip: Boolean = false + private fun getHeaders(): Map { - init { - headerMap["User-Agent"] = buildUserAgent() - } - - @SuppressLint("DefaultLocale") - private fun buildUserAgent(): String { - val versionName = "41.2.21-31" - val versionCode = "84122130" - val apiLevel = Build.VERSION.SDK_INT - val device = Build.DEVICE - val hardware = Build.HARDWARE - val product = Build.PRODUCT - val release = Build.VERSION.RELEASE - val model = Build.MODEL - val buildId = Build.ID - var supportedAbis: String? = null - supportedAbis = - if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { - java.lang.String.join(";", *Build.SUPPORTED_ABIS) - } else { - Build.CPU_ABI + ";" + Build.CPU_ABI2 - } - - return String.format( - "Android-Finsky/%s [0] [PR] 636997666 (api=%d,versionCode=%s,sdk=%d,device=%s,hardware=%s,product=%s,platformVersionRelease=%s,model=%s,buildId=%s,isWideScreen=%d,supportedAbis=%s)", - versionName,apiLevel,versionCode,apiLevel, - device, - hardware, - product, - release, - model, - buildId, - 0, - supportedAbis - ) - } - - private fun makeTimestamp(millis: Long): Timestamp? { - return Timestamp.Builder() - .seconds(millis / 1000) - .nanos(Math.floorMod(millis, 1000) * 1000000) - .build() - } + val auth = AccountManager.get(context).getAuthToken( + account, AUTH_TOKEN_SCOPE, null, false, null, null + ).result.getString(AccountManager.KEY_AUTHTOKEN) ?: "" - private fun getXHeaders(): String { - val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504"; - var millis = System.currentTimeMillis() - val timestamp = TimestampContainer.Builder() - .container2( - TimestampContainer2.Builder() - .wrapper( - TimestampWrapper.Builder() - .timestamp(makeTimestamp(millis)).build() - ) - .timestamp(makeTimestamp(millis)) - .build() - ) + if (auth.isEmpty()) { + Log.w(TAG, "authToken is Empty!") + } val androidId = SettingsContract.getSettings( context, @@ -119,100 +48,15 @@ class GoogleApiRequest( arrayOf(SettingsContract.CheckIn.ANDROID_ID) ) { cursor: Cursor -> cursor.getLong(0) } - millis = System.currentTimeMillis() - timestamp - .container1Wrapper( - TimestampContainer1Wrapper.Builder() - .androidId(androidId.toString()) - .container( - TimestampContainer1.Builder() - .timestamp(millis.toString() + "000") - .wrapper(makeTimestamp(millis)) - .build() - ) - .build() - ) - val encodedTimestamps = String( - Base64.encode( - Util.encodeGzip(timestamp.build().encode()), - Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING - ) - ) - val locality = Locality.Builder() - .unknown1(1) - .unknown2(2) - .countryCode("") - .region( - TimestampStringWrapper.Builder() - .string("") - .timestamp(makeTimestamp(System.currentTimeMillis())).build() - ) - .country( - TimestampStringWrapper.Builder() - .string("") - .timestamp(makeTimestamp(System.currentTimeMillis())).build() - ) - .unknown3(0) + val xPsRh = String(Base64.encode(getDefaultLicenseRequestHeaderBuilder(androidId) + .languages(RequestLanguagePackage.Builder().language(requestLanguagePackage).build()) .build() - val encodedLocality = String( - Base64.encode(locality.encode(), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING) - ) - val header = LicenseRequestHeader.Builder() - .encodedTimestamps(StringWrapper.Builder().string(encodedTimestamps).build()) - .triple( - EncodedTripleWrapper.Builder().triple( - EncodedTriple.Builder() - .encoded1("") - .encoded2("") - .empty("") - .build() - ).build() - ) - .locality(LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build()) - .unknown(IntWrapper.Builder().integer(5).build()) - .empty("") - .languages(externalxpsrh) - .deviceMeta( - DeviceMeta.Builder() - .android( - AndroidVersionMeta.Builder() - .androidSdk(org.microg.gms.profile.Build.VERSION.SDK_INT) - .buildNumber(org.microg.gms.profile.Build.ID) - .androidVersion(org.microg.gms.profile.Build.VERSION.RELEASE) - .unknown(0) - .build() - ) - .unknown1(UnknownByte12.Builder().bytes(ByteString.EMPTY).build()) - .unknown2(1) - .build() - ) - .userAgent( - UserAgent.Builder() - .deviceName(org.microg.gms.profile.Build.DEVICE) - .deviceHardware(org.microg.gms.profile.Build.HARDWARE) - .deviceModelName(org.microg.gms.profile.Build.MODEL) - .finskyVersion(FINSKY_VERSION) - .deviceProductName(org.microg.gms.profile.Build.MODEL) - .androidId(androidId) // must not be 0 - .buildFingerprint(org.microg.gms.profile.Build.FINGERPRINT) - .build() - ) - .uuid( - Uuid.Builder() - .uuid(UUID.randomUUID().toString()) - .unknown(2) - .build() - ) - .build().encode() - return String(Base64.encode(Util.encodeGzip(header), Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)) - } + .encode() + .encodeGzip(),Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING)) - private fun getHeaders(): Map { - headerMap["X-PS-RH"] = getXHeaders() - headerMap["Authorization"] = "Bearer " + AccountManager.get(context).getAuthToken( - user, tokenType, null, false, null, null - ).result.getString(AccountManager.KEY_AUTHTOKEN) - return this.headerMap + val headerMap = getLicenseRequestHeaders(auth, androidId).toMutableMap() + headerMap["X-PS-RH"] = xPsRh + return headerMap } fun sendRequest(externalHeader: Map?): GoogleApiResponse? { @@ -254,22 +98,10 @@ class GoogleApiRequest( } val responseCode = httpURLConnection.responseCode if (responseCode == HttpURLConnection.HTTP_OK) { - val data = toByteArray(httpURLConnection.inputStream) + val data = Utils.readStreamToEnd(httpURLConnection.inputStream) return GoogleApiResponse.ADAPTER.decode(data) } return null } - - private fun toByteArray(inputStream: InputStream): ByteArray { - val buffer = ByteArrayOutputStream() - var nRead: Int - val data = ByteArray(1024) - - while ((inputStream.read(data, 0, data.size).also { nRead = it }) != -1) { - buffer.write(data, 0, nRead) - } - buffer.flush() - return buffer.toByteArray() - } } \ No newline at end of file From b8d2b203df04fd569158222ae0359e844c06e9d3 Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Thu, 22 Aug 2024 20:27:49 +0800 Subject: [PATCH 06/11] SplitInstallService add call super.onBind --- .../android/finsky/splitinstallservice/SplitInstallService.kt | 1 + 1 file changed, 1 insertion(+) 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 defec77a7d..801f2d254f 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 @@ -20,6 +20,7 @@ class SplitInstallService : LifecycleService() { } override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) if (mService == null) { mService = SplitInstallServiceImpl(this.applicationContext) } From 6cdd488969f8e61214f2c18a9f3776c1dfcf20eb Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Mon, 26 Aug 2024 14:14:33 +0800 Subject: [PATCH 07/11] Fixed the issue that multiple versions of language packs could not be downloaded --- .../finsky/splitinstallservice/SplitInstallServiceImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 8798823f85..cae695ca79 100644 --- 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 @@ -353,7 +353,7 @@ class SplitInstallServiceImpl(private val context: Context) : ISplitInstallServi if (pkgs != null) { for (item in pkgs) { for (lang in langName) { - if (TextUtils.equals("config.$lang", item.splitPkgName)) { + if (TextUtils.equals("config.$lang", item.splitPkgName) || "config.$lang".startsWith(item.splitPkgName!!)) { downloadUrls.add(arrayOf(lang!!, item.downloadUrl1!!)) } } From 36651dbdcbea0b03614d592d9538d7ae5b1bc786 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 27 Aug 2024 19:56:38 +0800 Subject: [PATCH 08/11] 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; +//} From ff837d96a567ae1f921d3382132b2c52101a6c26 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Tue, 27 Aug 2024 20:00:35 +0800 Subject: [PATCH 09/11] Formatting Code --- .../src/main/java/org/microg/vending/billing/GServices.kt | 1 - .../google/android/finsky/splitinstallservice/extensions.kt | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/billing/GServices.kt b/vending-app/src/main/java/org/microg/vending/billing/GServices.kt index 9e43d0d506..60b151324d 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/GServices.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/GServices.kt @@ -7,7 +7,6 @@ import android.net.Uri object GServices { private val CONTENT_URI: Uri = Uri.parse("content://com.google.android.gsf.gservices") - fun getString(resolver: ContentResolver, key: String, defaultValue: String?): String? { var result = defaultValue val cursor = resolver.query(CONTENT_URI, null, null, arrayOf(key), null) 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 index 00264f812f..f3a4b93b11 100644 --- 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 @@ -78,8 +78,9 @@ 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() + if (lastSplitPackageName != null && lastSplitPackageName != pkg) { + if (mutex.isLocked) mutex.unlock() + splitRecord.clear() } mutex.withLock { Log.d(TAG, "trySplitInstall: pkg: $pkg") From 8a68044bd9243db108bdf838446d036933cf3002 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Thu, 12 Sep 2024 17:22:12 +0800 Subject: [PATCH 10/11] Execute processing suggestions --- vending-app/src/main/AndroidManifest.xml | 2 +- .../vending/licensing/LicenseChecker.kt | 31 +- .../licensing/LicenseRequestHeaders.kt | 181 --------- .../kotlin/com/android/vending/extensions.kt | 131 +++++++ .../SplitInstallManager.kt | 338 +++++++++++++++++ .../SplitInstallService.kt | 22 +- .../finsky/splitinstallservice/extensions.kt | 347 ------------------ ...censeRequest.proto => RequestHeader.proto} | 2 +- .../src/main/res/values-zh-rCN/strings.xml | 2 +- vending-app/src/main/res/values/strings.xml | 2 +- 10 files changed, 489 insertions(+), 569 deletions(-) delete mode 100644 vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt create mode 100644 vending-app/src/main/kotlin/com/android/vending/extensions.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt delete mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/extensions.kt rename vending-app/src/main/proto/{LicenseRequest.proto => RequestHeader.proto} (98%) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index f211be33f8..4530559240 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -184,7 +184,7 @@ diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt index 057a709e15..5567c39395 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt +++ b/vending-app/src/main/java/com/android/vending/licensing/LicenseChecker.kt @@ -2,20 +2,18 @@ package com.android.vending.licensing import android.accounts.Account import android.accounts.AccountManager -import android.accounts.AccountManagerFuture import android.accounts.AuthenticatorException import android.accounts.OperationCanceledException import android.content.pm.PackageInfo -import android.os.Bundle import android.os.RemoteException import android.util.Log +import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.LicenseResult +import com.android.vending.buildRequestHeaders +import com.android.vending.getAuthToken import com.android.volley.VolleyError import org.microg.vending.billing.core.HttpClient import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine private const val TAG = "FakeLicenseChecker" @@ -69,8 +67,6 @@ const val ERROR_INVALID_PACKAGE_NAME: Int = 0x102 */ const val ERROR_NON_MATCHING_UID: Int = 0x103 -const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay" - sealed class LicenseRequestParameters data class V1Parameters( val nonce: Long @@ -108,7 +104,7 @@ suspend fun HttpClient.checkLicense( ) : LicenseResponse { val auth = try { - accountManager.getAuthToken(account, AUTH_TOKEN_SCOPE, false) + getAuthToken(accountManager, account, AUTH_TOKEN_SCOPE) .getString(AccountManager.KEY_AUTHTOKEN) } catch (e: AuthenticatorException) { Log.e(TAG, "Could not fetch auth token for account $account") @@ -145,7 +141,7 @@ suspend fun HttpClient.makeLicenseV1Request( packageName: String, auth: String, versionCode: Int, nonce: Long, androidId: Long ): V1Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce", - headers = getLicenseRequestHeaders(auth, androidId), + headers = buildRequestHeaders(auth, androidId), adapter = LicenseResult.ADAPTER ).information?.v1?.let { if (it.result != null && it.signedData != null && it.signature != null) { @@ -160,22 +156,9 @@ suspend fun HttpClient.makeLicenseV2Request( androidId: Long ): V2Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode", - headers = getLicenseRequestHeaders(auth, androidId), + headers = buildRequestHeaders(auth, androidId), adapter = LicenseResult.ADAPTER ).information?.v2?.license?.jwt?.let { // Field present ←→ user has license V2Response(LICENSED, it) -} - - -suspend fun AccountManager.getAuthToken(account: Account, authTokenType: String, notifyAuthFailure: Boolean) = - suspendCoroutine { continuation -> - getAuthToken(account, authTokenType, notifyAuthFailure, { future: AccountManagerFuture -> - try { - val result = future.result - continuation.resume(result) - } catch (e: Exception) { - continuation.resumeWithException(e) - } - }, null) - } \ No newline at end of file +} \ No newline at end of file diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt b/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt deleted file mode 100644 index 0ae45beb1e..0000000000 --- a/vending-app/src/main/java/com/android/vending/licensing/LicenseRequestHeaders.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.android.vending.licensing - -import android.util.Base64 -import android.util.Log -import com.android.vending.AndroidVersionMeta -import com.android.vending.DeviceMeta -import com.android.vending.EncodedTriple -import com.android.vending.EncodedTripleWrapper -import com.android.vending.IntWrapper -import com.android.vending.LicenseRequestHeader -import com.android.vending.Locality -import com.android.vending.LocalityWrapper -import com.android.vending.StringWrapper -import com.android.vending.Timestamp -import com.android.vending.TimestampContainer -import com.android.vending.TimestampContainer1 -import com.android.vending.TimestampContainer1Wrapper -import com.android.vending.TimestampContainer2 -import com.android.vending.TimestampStringWrapper -import com.android.vending.TimestampWrapper -import com.android.vending.UnknownByte12 -import com.android.vending.UserAgent -import com.android.vending.Uuid -import com.google.android.gms.common.BuildConfig -import okio.ByteString -import org.microg.gms.profile.Build -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.net.URLEncoder -import java.util.UUID -import java.util.zip.GZIPOutputStream - -private const val TAG = "FakeLicenseRequest" - -private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING -private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" - -internal fun getDefaultLicenseRequestHeaderBuilder(androidId: Long) : LicenseRequestHeader.Builder { - var millis = System.currentTimeMillis() - val timestamp = TimestampContainer.Builder() - .container2( - TimestampContainer2.Builder() - .wrapper(TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build()) - .timestamp(makeTimestamp(millis)) - .build() - ) - millis = System.currentTimeMillis() - timestamp - .container1Wrapper( - TimestampContainer1Wrapper.Builder() - .androidId(androidId.toString()) - .container( - TimestampContainer1.Builder() - .timestamp(millis.toString() + "000") - .wrapper(makeTimestamp(millis)) - .build() - ) - .build() - ) - val encodedTimestamps = String( - Base64.encode(timestamp.build().encode().encodeGzip(), BASE64_FLAGS) - ) - - val locality = Locality.Builder() - .unknown1(1) - .unknown2(2) - .countryCode("") - .region( - TimestampStringWrapper.Builder() - .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build() - ) - .country( - TimestampStringWrapper.Builder() - .string("").timestamp(makeTimestamp(System.currentTimeMillis())).build() - ) - .unknown3(0) - .build() - val encodedLocality = String( - Base64.encode(locality.encode(), BASE64_FLAGS) - ) - - return LicenseRequestHeader.Builder() - .encodedTimestamps(StringWrapper.Builder().string(encodedTimestamps).build()) - .triple( - EncodedTripleWrapper.Builder().triple( - EncodedTriple.Builder() - .encoded1("") - .encoded2("") - .empty("") - .build() - ).build() - ) - .locality(LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build()) - .unknown(IntWrapper.Builder().integer(5).build()) - .empty("") - .deviceMeta( - DeviceMeta.Builder() - .android( - AndroidVersionMeta.Builder() - .androidSdk(Build.VERSION.SDK_INT) - .buildNumber(Build.ID) - .androidVersion(Build.VERSION.RELEASE) - .unknown(0) - .build() - ) - .unknown1( - UnknownByte12.Builder().bytes(ByteString.EMPTY).build() - ) - .unknown2(1) - .build() - ) - .userAgent( - UserAgent.Builder() - .deviceName(Build.DEVICE) - .deviceHardware(Build.HARDWARE) - .deviceModelName(Build.MODEL) - .finskyVersion(FINSKY_VERSION) - .deviceProductName(Build.MODEL) - .androidId(androidId) // must not be 0 - .buildFingerprint(Build.FINGERPRINT) - .build() - ) - .uuid( - Uuid.Builder() - .uuid(UUID.randomUUID().toString()) - .unknown(2) - .build() - ) -} - -internal fun getLicenseRequestHeaders(auth: String, androidId: Long): Map { - val header = getDefaultLicenseRequestHeaderBuilder(androidId).build().encode() - val xPsRh = String(Base64.encode(header.encodeGzip(), BASE64_FLAGS)) - - Log.v(TAG, "X-PS-RH: $xPsRh") - - val userAgent = - "$FINSKY_VERSION (api=3,versionCode=${BuildConfig.VERSION_CODE},sdk=${Build.VERSION.SDK}," + - "device=${encodeString(Build.DEVICE)},hardware=${encodeString(Build.HARDWARE)}," + - "product=${encodeString(Build.PRODUCT)},platformVersionRelease=${encodeString(Build.VERSION.RELEASE)}," + - "model=${encodeString(Build.MODEL)},buildId=${encodeString(Build.ID)},isWideScreen=${0}," + - "supportedAbis=${Build.SUPPORTED_ABIS.joinToString(";")})" - Log.v(TAG, "User-Agent: $userAgent") - - return mapOf( - "X-PS-RH" to xPsRh, - "User-Agent" to userAgent, - "Authorization" to "Bearer $auth", - "Accept-Language" to "en-US", - "Connection" to "Keep-Alive" - ) -} - -private fun makeTimestamp(millis: Long): Timestamp { - return Timestamp.Builder() - .seconds((millis / 1000)) - .nanos(((millis % 1000) * 1000000).toInt()) - .build() -} - -private fun encodeString(s: String?): String { - return URLEncoder.encode(s).replace("+", "%20") -} - -/** - * From [StackOverflow](https://stackoverflow.com/a/46688434/), CC BY-SA 4.0 by Sergey Frolov, adapted. - */ -fun ByteArray.encodeGzip(): ByteArray { - try { - ByteArrayOutputStream().use { byteOutput -> - GZIPOutputStream(byteOutput).use { gzipOutput -> - gzipOutput.write(this) - gzipOutput.finish() - return byteOutput.toByteArray() - } - } - } catch (e: IOException) { - Log.e(TAG, "Failed to encode bytes as GZIP") - return ByteArray(0) - } -} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/android/vending/extensions.kt b/vending-app/src/main/kotlin/com/android/vending/extensions.kt new file mode 100644 index 0000000000..5b010d2c32 --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/extensions.kt @@ -0,0 +1,131 @@ +/** + * SPDX-FileCopyrightText: 2024 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.vending + +import android.accounts.Account +import android.accounts.AccountManager +import android.accounts.AccountManagerFuture +import android.os.Bundle +import android.util.Base64 +import android.util.Log +import com.google.android.gms.common.BuildConfig +import okio.ByteString +import org.microg.gms.profile.Build +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.net.URLEncoder +import java.util.UUID +import java.util.zip.GZIPOutputStream +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +private const val TAG = "FakeLicenseRequest" + +const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/googleplay" + +private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING +private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" + +fun buildRequestHeaders(auth: String, androidId: Long, language: List ?= null): Map { + var millis = System.currentTimeMillis() + val timestamp = TimestampContainer.Builder().container2( + TimestampContainer2.Builder().wrapper(TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build()).timestamp(makeTimestamp(millis)).build() + ) + millis = System.currentTimeMillis() + timestamp.container1Wrapper( + TimestampContainer1Wrapper.Builder().androidId(androidId.toString()).container( + TimestampContainer1.Builder().timestamp(millis.toString() + "000").wrapper(makeTimestamp(millis)).build() + ).build() + ) + + val encodedTimestamps = String(Base64.encode(timestamp.build().encode().encodeGzip(), BASE64_FLAGS)) + val locality = Locality.Builder().unknown1(1).unknown2(2).countryCode("").region( + TimestampStringWrapper.Builder().string("").timestamp(makeTimestamp(System.currentTimeMillis())).build() + ).country( + TimestampStringWrapper.Builder().string("").timestamp(makeTimestamp(System.currentTimeMillis())).build() + ).unknown3(0).build() + val encodedLocality = String( + Base64.encode(locality.encode(), BASE64_FLAGS) + ) + + val header = RequestHeader.Builder().encodedTimestamps(StringWrapper.Builder().string(encodedTimestamps).build()).triple( + EncodedTripleWrapper.Builder().triple( + EncodedTriple.Builder().encoded1("").encoded2("").empty("").build() + ).build() + ).locality(LocalityWrapper.Builder().encodedLocalityProto(encodedLocality).build()).unknown(IntWrapper.Builder().integer(5).build()).empty("").deviceMeta( + DeviceMeta.Builder().android( + AndroidVersionMeta.Builder().androidSdk(Build.VERSION.SDK_INT).buildNumber(Build.ID).androidVersion(Build.VERSION.RELEASE).unknown(0).build() + ).unknown1( + UnknownByte12.Builder().bytes(ByteString.EMPTY).build() + ).unknown2(1).build() + ).userAgent( + UserAgent.Builder().deviceName(Build.DEVICE).deviceHardware(Build.HARDWARE).deviceModelName(Build.MODEL).finskyVersion(FINSKY_VERSION) + .deviceProductName(Build.MODEL).androidId(androidId) // must not be 0 + .buildFingerprint(Build.FINGERPRINT).build() + ).uuid( + Uuid.Builder().uuid(UUID.randomUUID().toString()).unknown(2).build() + ).apply { + if (language != null) { + languages( + RequestLanguagePackage.Builder().language(language).build() + ) + } + }.build().encode() + + val xPsRh = String(Base64.encode(header.encodeGzip(), BASE64_FLAGS)) + Log.v(TAG, "X-PS-RH: $xPsRh") + val userAgent = + "$FINSKY_VERSION (api=3,versionCode=${BuildConfig.VERSION_CODE},sdk=${Build.VERSION.SDK}," + "device=${encodeString(Build.DEVICE)},hardware=${ + encodeString(Build.HARDWARE) + }," + "product=${encodeString(Build.PRODUCT)},platformVersionRelease=${encodeString(Build.VERSION.RELEASE)}," + "model=${encodeString(Build.MODEL)},buildId=${ + encodeString( + Build.ID + ) + },isWideScreen=${0}," + "supportedAbis=${Build.SUPPORTED_ABIS.joinToString(";")})" + Log.v(TAG, "User-Agent: $userAgent") + + return mapOf( + "X-PS-RH" to xPsRh, "User-Agent" to userAgent, "Authorization" to "Bearer $auth", "Accept-Language" to "en-US", "Connection" to "Keep-Alive" + ) +} + +private fun makeTimestamp(millis: Long): Timestamp { + return Timestamp.Builder().seconds((millis / 1000)).nanos(((millis % 1000) * 1000000).toInt()).build() +} + +private fun encodeString(s: String?): String { + return URLEncoder.encode(s).replace("+", "%20") +} + +/** + * From [StackOverflow](https://stackoverflow.com/a/46688434/), CC BY-SA 4.0 by Sergey Frolov, adapted. + */ +fun ByteArray.encodeGzip(): ByteArray { + try { + ByteArrayOutputStream().use { byteOutput -> + GZIPOutputStream(byteOutput).use { gzipOutput -> + gzipOutput.write(this) + gzipOutput.finish() + return byteOutput.toByteArray() + } + } + } catch (e: IOException) { + Log.e(TAG, "Failed to encode bytes as GZIP") + return ByteArray(0) + } +} +suspend fun getAuthToken(accountManager: AccountManager, account: Account, authTokenType: String) = + suspendCoroutine { continuation -> + accountManager.getAuthToken(account, authTokenType, false, { future: AccountManagerFuture -> + try { + val result = future.result + continuation.resume(result) + } catch (e: Exception) { + continuation.resumeWithException(e) + } + }, null) + } diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt new file mode 100644 index 0000000000..8f8803b155 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt @@ -0,0 +1,338 @@ +/** + * 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.os.Build +import android.os.Bundle +import android.util.ArraySet +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.collection.arraySetOf +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat +import androidx.core.content.pm.PackageInfoCompat +import com.android.vending.AUTH_TOKEN_SCOPE +import com.android.vending.R +import com.android.vending.buildRequestHeaders +import com.android.vending.getAuthToken +import com.google.android.finsky.GoogleApiResponse +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE +import org.microg.vending.billing.core.HttpClient +import java.io.File +import java.io.FileInputStream +import java.io.IOException + +private const val SPLIT_INSTALL_NOTIFY_ID = 111 +private const val SPLIT_INSTALL_REQUEST_TAG = "splitInstallRequestTag" +private const val SPLIT_LANGUAGE_TAG = "config." + +private const val NOTIFY_CHANNEL_ID = "splitInstall" +private const val NOTIFY_CHANNEL_NAME = "Split Install" +private const val KEY_LANGUAGE = "language" +private const val KEY_LANGUAGES = "languages" +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 STATUS_UNKNOWN = -1 +private const val STATUS_DOWNLOADING = 0 +private const val STATUS_DOWNLOADED = 1 + +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 = "SplitInstallManager" + +class SplitInstallManager(val context: Context) { + + private var httpClient: HttpClient = HttpClient(context) + + suspend fun startInstall(callingPackage: String, splits: List): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false +// val callingPackage = runCatching { PackageUtils.getAndCheckCallingPackage(context, packageName) }.getOrNull() ?: return + if (splits.all { it.getString(KEY_LANGUAGE) == null && it.getString(KEY_MODULE_NAME) == null }) return false + Log.d(TAG, "startInstall: start") + val needInstallSplitPack = arraySetOf() + for (split in splits) { + val splitName = split.getString(KEY_LANGUAGE)?.let { "$SPLIT_LANGUAGE_TAG$it" } ?: split.getString(KEY_MODULE_NAME) ?: continue + val splitInstalled = checkSplitInstalled(callingPackage, splitName) + if (splitInstalled) continue + needInstallSplitPack.add(splitName) + } + Log.d(TAG, "startInstall needInstallSplitPack: $needInstallSplitPack") + if (needInstallSplitPack.isEmpty()) return false + val oauthToken = runCatching { withContext(Dispatchers.IO) { getOauthToken() } }.getOrNull() + Log.d(TAG, "startInstall oauthToken: $oauthToken") + if (oauthToken.isNullOrEmpty()) return false + notify(context) + val triples = runCatching { requestDownloadUrls(callingPackage, oauthToken, needInstallSplitPack) }.getOrNull() + Log.w(TAG, "startInstall requestDownloadUrls triples: $triples") + if (triples.isNullOrEmpty()) { + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + return false + } + val intent = runCatching { installSplitPackage(context, callingPackage, triples) }.getOrNull() + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + if (intent == null) { return false } + sendCompleteBroad(context, callingPackage, intent) + return true + } + + @RequiresApi(Build.VERSION_CODES.M) + private suspend fun installSplitPackage(context: Context, callingPackage: String, downloadList: ArraySet>): Intent { + Log.d(TAG, "installSplitPackage start ") + if (!context.splitSaveFile().exists()) context.splitSaveFile().mkdir() + val downloadSplitPackage = downloadSplitPackage(context, callingPackage, downloadList) + if (!downloadSplitPackage) { + Log.w(TAG, "installSplitPackage download failed") + throw RuntimeException("installSplitPackage downloadSplitPackage has error") + } + Log.d(TAG, "installSplitPackage downloaded success") + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + + val packageInstaller = context.packageManager.packageInstaller + val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) + params.setAppPackageName(callingPackage) + params.setAppLabel(callingPackage + "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) + downloadList.forEach { item -> + val pkgPath = File(context.splitSaveFile().toString(), item.first) + session.openWrite(item.first, 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_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() + } + } + + @RequiresApi(Build.VERSION_CODES.M) + private suspend fun downloadSplitPackage(context: Context, callingPackage: String, downloadList: ArraySet>): Boolean = + coroutineScope { + val results = downloadList.map { info -> + Log.d(TAG, "downloadSplitPackage: $info") + async { + val downloaded = runCatching { + httpClient.download(info.second, File(context.splitSaveFile().toString(), info.first), SPLIT_INSTALL_REQUEST_TAG) + }.onFailure { + Log.w(TAG, "downloadSplitPackage url:${info.second} save:${info.first}", it) + }.getOrNull() != null + downloaded.also { updateSplitInstallRecord(callingPackage, Triple(info.first, info.second, if (it) STATUS_DOWNLOADED else STATUS_UNKNOWN)) } + } + }.awaitAll() + return@coroutineScope results.all { it } + } + + @RequiresApi(Build.VERSION_CODES.M) + private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, packs: MutableSet): ArraySet> { + val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(callingPackage, 0)) + val requestUrl = + StringBuilder("https://play-fe.googleapis.com/fdfe/delivery?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=") + packs.forEach { requestUrl.append("&mn=").append(it) } + Log.d(TAG, "requestDownloadUrls start") + val languages = packs.filter { it.startsWith(SPLIT_LANGUAGE_TAG) }.map { it.replace(SPLIT_LANGUAGE_TAG, "") } + Log.d(TAG, "requestDownloadUrls languages: $languages") + val response = httpClient.get( + url = requestUrl.toString(), + headers = buildRequestHeaders(authToken, 1, languages).onEach { Log.d(TAG, "key:${it.key} value:${it.value}") }, + adapter = GoogleApiResponse.ADAPTER + ) + Log.d(TAG, "requestDownloadUrls end response -> $response") + val splitPkgInfoList = response.response?.splitReqResult?.pkgList?.pkgDownLoadInfo ?: throw RuntimeException("splitPkgInfoList is null") + val packSet = ArraySet>() + splitPkgInfoList.filter { + !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() + }.forEach { info -> + packs.filter { + it.contains(info.splitPkgName!!) + }.forEach { + packSet.add(Triple(first = it, second = info.downloadUrl!!, STATUS_DOWNLOADING)) + } + } + Log.d(TAG, "requestDownloadUrls end packSet -> $packSet") + return packSet.onEach { updateSplitInstallRecord(callingPackage, it) } + } + + private suspend fun getOauthToken(): String { + val accounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE) + var oauthToken: String? = null + if (accounts.isEmpty()) { + Log.w(TAG, "No Google account found") + throw RuntimeException("No Google account found") + } else for (account: Account in accounts) { + oauthToken = try { + getAuthToken(AccountManager.get(context), account, AUTH_TOKEN_SCOPE).getString(AccountManager.KEY_AUTHTOKEN) + } catch (e: AuthenticatorException) { + Log.w(TAG, "Could not fetch auth token for account $account") + null + } + if (oauthToken != null) { + break + } + } + return oauthToken ?: throw RuntimeException("oauthToken is null") + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun checkSplitInstalled(callingPackage: String, splitName: String): Boolean { + if (!splitInstallRecord.containsKey(splitName)) return false + return splitInstallRecord[callingPackage]?.find { it.first == splitName }?.third != STATUS_UNKNOWN + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun updateSplitInstallRecord(callingPackage: String, triple: Triple) { + splitInstallRecord[callingPackage]?.let { triples -> + val find = triples.find { it.first == triple.first } + find?.let { triples.remove(it) } + triples.add(triple) + } ?: run { + val triples = ArraySet>() + triples.add(triple) + splitInstallRecord[callingPackage] = triples + } + } + + 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 fun Context.splitSaveFile() = File(filesDir, FILE_SAVE_PATH) + + private fun sendCompleteBroad(context: Context, packageName: String, intent: Intent) { + Log.d(TAG, "sendCompleteBroadcast: intent:$intent") + val extra = Bundle().apply { + putInt(KEY_STATUS, 5) + putInt(KEY_ERROR_CODE, 0) + putInt(KEY_SESSION_ID, 0) + putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0)) + putString(KEY_LANGUAGES, intent.getStringExtra(KEY_LANGUAGE)) + putLong(KEY_BYTES_DOWNLOADED, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0)) + } + val broadcastIntent = Intent(ACTION_UPDATE_SERVICE).apply { + setPackage(packageName) + putExtra(KEY_SESSION_STATE, extra) + addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) + } + context.sendBroadcast(broadcastIntent) + } + + fun release() { + httpClient.requestQueue.cancelAll(SPLIT_INSTALL_REQUEST_TAG) + splitInstallRecord.clear() + deferredMap.clear() + } + + internal class InstallResultReceiver : BroadcastReceiver() { + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + override fun onReceive(context: Context, intent: Intent) { + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) + val sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) + Log.d(TAG, "onReceive status: $status sessionId: $sessionId") + try { + when (status) { + PackageInstaller.STATUS_SUCCESS -> { + Log.d(TAG, "InstallResultReceiver onReceive: install success") + if (sessionId != -1) { + deferredMap[sessionId]?.complete(intent) + deferredMap.remove(sessionId) + } + } + + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + val extraIntent = intent.extras?.getParcelable(Intent.EXTRA_INTENT) as Intent? + extraIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + extraIntent?.run { ContextCompat.startActivity(context, this, null) } + } + + else -> { + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + Log.w(TAG, "InstallResultReceiver onReceive: install fail -> $errorMsg") + if (sessionId != -1) { + deferredMap[sessionId]?.completeExceptionally(RuntimeException("install fail -> $errorMsg")) + deferredMap.remove(sessionId) + } + } + } + } catch (e: Exception) { + Log.w(TAG, "Error handling install result", e) + if (sessionId != -1) { + deferredMap[sessionId]?.completeExceptionally(e) + } + } + } + } + + companion object { + // Installation records, including subpackage name, download path, and installation status + private val splitInstallRecord = HashMap>>() + private val deferredMap = mutableMapOf>() + } +} 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 87a168211c..c49356e723 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,7 +5,6 @@ package com.google.android.finsky.splitinstallservice -import android.content.Context import android.content.Intent import android.os.Bundle import android.os.IBinder @@ -19,36 +18,36 @@ 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" +private const val TAG = "SplitInstallService" class SplitInstallService : LifecycleService() { - private lateinit var httpClient: HttpClient + private lateinit var splitInstallManager: SplitInstallManager override fun onBind(intent: Intent): IBinder? { super.onBind(intent) Log.d(TAG, "onBind: ") ProfileManager.ensureInitialized(this) - httpClient = HttpClient(this) - return SplitInstallServiceImpl(this.applicationContext, httpClient, lifecycle).asBinder() + splitInstallManager = SplitInstallManager(this) + return SplitInstallServiceImpl(splitInstallManager, lifecycle).asBinder() } override fun onUnbind(intent: Intent?): Boolean { Log.d(TAG, "onUnbind: ") - httpClient.requestQueue.cancelAll(SPLIT_INSTALL_REQUEST_TAG) + splitInstallManager.release() return super.onUnbind(intent) } } -class SplitInstallServiceImpl(private val context: Context, private val httpClient: HttpClient, override val lifecycle: Lifecycle) : ISplitInstallService.Stub(), LifecycleOwner { +class SplitInstallServiceImpl(private val installManager: SplitInstallManager, 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") + val installStatus = installManager.startInstall(pkg, splits) + Log.d(TAG, "startInstall: installStatus -> $installStatus") callback.onStartInstall(CommonStatusCodes.SUCCESS, Bundle()) } } @@ -97,9 +96,6 @@ class SplitInstallServiceImpl(private val context: Context, private val httpClie 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) - } } override fun languageSplitUninstall(pkg: String, splits: List, callback: ISplitInstallServiceCallback) { 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 deleted file mode 100644 index f3a4b93b11..0000000000 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/extensions.kt +++ /dev/null @@ -1,347 +0,0 @@ -/** - * 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) { - if (mutex.isLocked) mutex.unlock() - splitRecord.clear() - } - 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/proto/LicenseRequest.proto b/vending-app/src/main/proto/RequestHeader.proto similarity index 98% rename from vending-app/src/main/proto/LicenseRequest.proto rename to vending-app/src/main/proto/RequestHeader.proto index c46d5d2d44..4fb678a558 100644 --- a/vending-app/src/main/proto/LicenseRequest.proto +++ b/vending-app/src/main/proto/RequestHeader.proto @@ -3,7 +3,7 @@ syntax = "proto2"; option java_package = "com.android.vending"; option java_multiple_files = true; -message LicenseRequestHeader { +message RequestHeader { optional StringWrapper encodedTimestamps = 1; optional EncodedTripleWrapper triple = 10; optional LocalityWrapper locality = 11; diff --git a/vending-app/src/main/res/values-zh-rCN/strings.xml b/vending-app/src/main/res/values-zh-rCN/strings.xml index 93dae0efe9..f0554d99ee 100644 --- a/vending-app/src/main/res/values-zh-rCN/strings.xml +++ b/vending-app/src/main/res/values-zh-rCN/strings.xml @@ -18,5 +18,5 @@ 如果应用出现异常,请登录您购买该应用所使用的 Google 帐号。 登录 忽略 - %s 正在下载分包 + 正在下载 %s 所需的组件 \ No newline at end of file diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 521b81fd18..0393927deb 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -28,5 +28,5 @@ Forget password? Learn more Verify - %s Downloading subpackages + Downloading required components for %s From 5a9f1b3d8a2a4b711cfbaed94808c338005e4d84 Mon Sep 17 00:00:00 2001 From: davinci9196 Date: Thu, 12 Sep 2024 17:39:26 +0800 Subject: [PATCH 11/11] cleanup --- .../android/finsky/splitinstallservice/SplitInstallManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt index 8f8803b155..aa92d5ef7c 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt @@ -113,7 +113,6 @@ class SplitInstallManager(val context: Context) { throw RuntimeException("installSplitPackage downloadSplitPackage has error") } Log.d(TAG, "installSplitPackage downloaded success") - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) val packageInstaller = context.packageManager.packageInstaller val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) @@ -229,7 +228,7 @@ class SplitInstallManager(val context: Context) { @RequiresApi(Build.VERSION_CODES.M) private fun checkSplitInstalled(callingPackage: String, splitName: String): Boolean { - if (!splitInstallRecord.containsKey(splitName)) return false + if (!splitInstallRecord.containsKey(callingPackage)) return false return splitInstallRecord[callingPackage]?.find { it.first == splitName }?.third != STATUS_UNKNOWN }