From 34ccbe2c00a1231aa7ec4dcd967843eebb2a5da8 Mon Sep 17 00:00:00 2001 From: DaVinci9196 Date: Mon, 19 Aug 2024 19:22:28 +0800 Subject: [PATCH 01/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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 } From 7df6f93bdb5c7bdc34d5640e4bb38f93a405c881 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 22 Aug 2024 11:12:15 +0200 Subject: [PATCH 12/59] Location/Huawei: Fix permission notification cancelling --- .../AskPermissionNotificationActivity.kt | 57 ++++++------------- 1 file changed, 16 insertions(+), 41 deletions(-) diff --git a/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt b/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt index 8daf22dab0..6c3991179e 100644 --- a/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt +++ b/play-services-location/core/src/huawei/kotlin/org/microg/gms/location/manager/AskPermissionNotificationActivity.kt @@ -5,16 +5,13 @@ package org.microg.gms.location.manager -import android.Manifest import android.Manifest.permission.* import android.annotation.TargetApi import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.graphics.Color import android.graphics.Typeface @@ -34,13 +31,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.PendingIntentCompat -import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED import androidx.core.content.getSystemService -import org.microg.gms.location.core.R import org.microg.gms.location.core.BuildConfig +import org.microg.gms.location.core.R import org.microg.gms.utils.getApplicationLabel -import java.lang.Exception + +private const val ACTION_ASK = "org.microg.gms.location.manager.ASK_PERMISSION" +private const val ACTION_CANCEL = "org.microg.gms.location.manager.ASK_PERMISSION_CANCEL" @RequiresApi(23) class AskPermissionNotificationActivity : AppCompatActivity() { @@ -56,12 +53,20 @@ class AskPermissionNotificationActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + if (intent?.action == ACTION_CANCEL) { + hideLocationPermissionNotification(this) + finish() + return + } + setContentView(R.layout.extended_permission_request) rationaleTextView = findViewById(R.id.rationale_textview) if (checkAllPermissions()) { hideLocationPermissionNotification(this) finish() + return } else if (isGranted(ACCESS_COARSE_LOCATION) && isGranted(ACCESS_FINE_LOCATION) && !isGranted(ACCESS_BACKGROUND_LOCATION) && SDK_INT >= 29) { requestBackground() } else { @@ -216,13 +221,11 @@ class AskPermissionNotificationActivity : AppCompatActivity() { private const val SHARED_PREFERENCE_NAME = "location_perm_notify" const val PERMISSION_REJECT_SHOW = "permission_reject_show" private const val NOTIFICATION_ID = 1026359765 - - private var notificationIsShown = false + private const val ASK_REQUEST_CODE = 1026359766 + private const val CANCEL_REQUEST_CODE = 1026359767 @JvmStatic fun showLocationPermissionNotification(context: Context) { - if (notificationIsShown) return - AskPermissionNotificationCancel.register(context) val appName = context.packageManager.getApplicationLabel(context.packageName).toString() val title = context.getString(R.string.location_permission_notification_title, appName) val backgroundPermissionOption = @@ -231,22 +234,18 @@ class AskPermissionNotificationActivity : AppCompatActivity() { val notification = NotificationCompat.Builder(context, createNotificationChannel(context)) .setContentTitle(title).setContentText(text) .setSmallIcon(R.drawable.ic_permission_notification) - .setContentIntent(PendingIntentCompat.getActivity(context, 0, Intent(context, AskPermissionNotificationActivity::class.java), 0, false)) + .setContentIntent(PendingIntentCompat.getActivity(context, ASK_REQUEST_CODE, Intent(context, AskPermissionNotificationActivity::class.java).apply { action = ACTION_ASK }, 0, false)) .setStyle(NotificationCompat.BigTextStyle().bigText(text)) .setPriority(NotificationCompat.PRIORITY_HIGH) .setOngoing(true) - .setDeleteIntent(PendingIntentCompat.getBroadcast(context, 0, AskPermissionNotificationCancel.getTrigger(context), 0, false)) + .setDeleteIntent(PendingIntentCompat.getActivity(context, CANCEL_REQUEST_CODE, Intent(context, AskPermissionNotificationActivity::class.java).apply { action = ACTION_CANCEL}, 0, false)) .build() context.getSystemService()?.notify(NOTIFICATION_ID, notification) - notificationIsShown = true } @JvmStatic fun hideLocationPermissionNotification(context: Context) { - if (!notificationIsShown) return context.getSystemService()?.cancel(NOTIFICATION_ID) - AskPermissionNotificationCancel.unregister(context) - notificationIsShown = false } @TargetApi(26) @@ -268,28 +267,4 @@ class AskPermissionNotificationActivity : AppCompatActivity() { } } -} - -@RequiresApi(23) -private object AskPermissionNotificationCancel : BroadcastReceiver() { - private const val ACTION = "org.microg.gms.location.manager.ASK_PERMISSION_CANCEL" - override fun onReceive(context: Context, intent: Intent?) { - AskPermissionNotificationActivity.hideLocationPermissionNotification(context) - } - - fun getTrigger(context: Context): Intent { - return Intent(ACTION).apply { `package` = context.packageName } - } - - fun register(context: Context) { - ContextCompat.registerReceiver(context, this, IntentFilter(ACTION), RECEIVER_NOT_EXPORTED) - } - - fun unregister(context: Context) { - try { - context.unregisterReceiver(this) - } catch (e: Exception) { - Log.w(TAG, e) - } - } } \ No newline at end of file From bc57d7828279c78f2d4d96d29e5ab8fc9828b732 Mon Sep 17 00:00:00 2001 From: DaVinci9196 <150454414+DaVinci9196@users.noreply.github.com> Date: Thu, 22 Aug 2024 17:26:56 +0800 Subject: [PATCH 13/59] Added the automatic login function for Games (#2435) Co-authored-by: Marvin W --- .../src/main/AndroidManifest.xml | 1 + .../gms/auth/signin/AssistedSignInFragment.kt | 4 +- .../org/microg/gms/auth/signin/extensions.kt | 5 +- .../microg/gms/games/GamesConnectService.kt | 59 ++++++++++++++----- .../org/microg/gms/games/GamesService.kt | 10 ++-- .../gms/games/internal/IGamesService.aidl | 2 + 6 files changed, 56 insertions(+), 25 deletions(-) diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 8b2295d3b1..a882f9fc4a 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -402,6 +402,7 @@ android:theme="@style/Theme.App.DayNight.Dialog.Alert.NoActionBar"> + diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt index d3e03a0849..673197229f 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/AssistedSignInFragment.kt @@ -84,7 +84,7 @@ class AssistedSignInFragment( val allowAutoLoginAccounts = mutableListOf() runCatching { accounts.forEach { account -> - val authStatus = checkAppAuthStatus(requireContext(), clientPackageName, options, account) + val authStatus = checkAccountAuthStatus(requireContext(), clientPackageName, options.scopes, account) if (authStatus) { allowAutoLoginAccounts.add(account) } @@ -287,4 +287,4 @@ class AssistedSignInFragment( } } -} \ No newline at end of file +} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt index f6ce68c522..4c860430e5 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/auth/signin/extensions.kt @@ -94,8 +94,9 @@ fun getServerAuthTokenManager(context: Context, packageName: String, options: Go return serverAuthTokenManager } -suspend fun checkAppAuthStatus(context: Context, packageName: String, options: GoogleSignInOptions?, account: Account): Boolean { - val authManager = getOAuthManager(context, packageName, options, account) +suspend fun checkAccountAuthStatus(context: Context, packageName: String, scopeList: List?, account: Account): Boolean { + val scopes = scopeList.orEmpty().sortedBy { it.scopeUri } + val authManager = AuthManager(context, account.name, packageName, "oauth2:${scopes.joinToString(" ")}") authManager.ignoreStoredPermission = true return withContext(Dispatchers.IO) { authManager.requestAuth(true) }.auth != null } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt index 34810ef768..477dae80f9 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesConnectService.kt @@ -6,6 +6,7 @@ package org.microg.gms.games import android.accounts.Account +import android.accounts.AccountManager import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -17,6 +18,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.google.android.gms.common.Scopes import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Scope import com.google.android.gms.common.api.Status import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest @@ -26,8 +28,10 @@ import com.google.android.gms.games.internal.connect.GamesSignInResponse import com.google.android.gms.games.internal.connect.IGamesConnectCallbacks import com.google.android.gms.games.internal.connect.IGamesConnectService import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants import org.microg.gms.auth.AuthManager import org.microg.gms.auth.AuthPrefs +import org.microg.gms.auth.signin.checkAccountAuthStatus import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils import org.microg.gms.utils.warnOnTransactionIssues @@ -61,8 +65,9 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED, null, resolution), null) } - 1 -> { // Auto sign-in on start, don't provide resolution if not - callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED), null) + 1 -> { // Automatically try to log in with a supported account at startup, + // and provide an account selection solution if verification fails + callback?.onSignIn(Status(CommonStatusCodes.SIGN_IN_REQUIRED, null, resolution), null) } else -> { @@ -71,23 +76,45 @@ class GamesConnectServiceImpl(val context: Context, override val lifecycle: Life } } lifecycleScope.launchWhenStarted { - try { - val account = request?.previousStepResolutionResult?.resultData?.getParcelableExtra(EXTRA_ACCOUNT) + val status = autoSelectLogin(request) + if (status) { + Log.d(TAG, "signIn success") + callback?.onSignIn(Status.SUCCESS, GamesSignInResponse().apply { gameRunToken = UUID.randomUUID().toString() }) + } else { + sendSignInRequired() + } + } + } + + private suspend fun autoSelectLogin(request: GamesSignInRequest?): Boolean { + runCatching { + var account = request?.previousStepResolutionResult?.resultData?.getParcelableExtra(EXTRA_ACCOUNT) ?: GamesConfigurationService.getDefaultAccount(context, packageName) - ?: return@launchWhenStarted sendSignInRequired() - val authManager = AuthManager(context, account.name, packageName, "oauth2:${Scopes.GAMES_LITE}") - if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return@launchWhenStarted sendSignInRequired() - val result = performGamesSignIn(context, packageName, account) - if (result) { - callback?.onSignIn(Status.SUCCESS, GamesSignInResponse().apply { gameRunToken = UUID.randomUUID().toString() }) - } else { - sendSignInRequired() - } - } catch (e: Exception) { - Log.w(TAG, e) - return@launchWhenStarted sendSignInRequired() + Log.d(TAG, "autoSelectLogin signInType: ${request?.signInType} account: $account") + val autoLogin = if (account == null && request?.signInType == 1) { + val accounts = AccountManager.get(context).getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE) + account = accounts.filter { targetAccount -> + checkAccountAuthStatus(context, packageName, arrayListOf(Scope(Scopes.GAMES_LITE)), targetAccount) + }.getOrNull(0) + true + } else { + false + } + if (account == null) { + Log.d(TAG, "autoSelectLogin Accounts is Empty") + return false + } + val authManager = AuthManager(context, account.name, packageName, "oauth2:${Scopes.GAMES_LITE}") + if (!authManager.isPermitted && !AuthPrefs.isTrustGooglePermitted(context)) return false + val performGamesSignInStatus = performGamesSignIn(context, packageName, account) + if (performGamesSignInStatus && autoLogin) { + GamesConfigurationService.setDefaultAccount(context, packageName, account) } + return performGamesSignInStatus + }.onFailure { + Log.d(TAG, "autoSelectLogin fail", it) } + return false } override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean = diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt index 9fb6e16bd1..e4d4cad46a 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -114,7 +114,7 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, } override fun signOut(callbacks: IGamesCallbacks?) { - Log.d(TAG, "Not yet implemented: signOut") + Log.d(TAG, "signOut called") lifecycleScope.launchWhenStarted { GamesConfigurationService.setDefaultAccount(context, packageName, null) callbacks?.onSignOutComplete() @@ -140,8 +140,8 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, } override fun getCurrentAccountName(): String? { - Log.d(TAG, "Not yet implemented: getCurrentAccountName") - return null + Log.d(TAG, "getCurrentAccountName called: ${account.name}") + return account.name } override fun loadGameplayAclInternal(callbacks: IGamesCallbacks?, gameId: String?) { @@ -161,8 +161,8 @@ class GamesServiceImpl(val context: Context, override val lifecycle: Lifecycle, } override fun getCurrentPlayerId(): String? { - Log.d(TAG, "Not yet implemented: getCurrentPlayerId") - return null + Log.d(TAG, "getCurrentPlayerId called: ${player.playerId}") + return player.playerId } override fun getCurrentPlayer(): DataHolder? { diff --git a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl index 010978f624..64d3579ef8 100644 --- a/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl +++ b/play-services-games/src/main/aidl/com/google/android/gms/games/internal/IGamesService.aidl @@ -92,6 +92,8 @@ interface IGamesService { Intent getAchievementsIntent() = 9004; Intent getPlayerSearchIntent() = 9009; +// void getSelectSnapshotIntent(String str, boolean z, boolean z2, int i) = 12001; +// void loadSnapshotsResult(IGamesCallbacks callbacks, boolean forceReload) = 12002; void loadEvents(IGamesCallbacks callbacks, boolean forceReload) = 12015; void incrementEvent(String eventId, int incrementAmount) = 12016; // void discardAndCloseSnapshot(in Contents contents) = 12018; From a7a258458d23ed1e53adece812a74ccd9137a173 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Thu, 22 Aug 2024 13:14:33 +0200 Subject: [PATCH 14/59] Location: Make sure current interval and configured interval stay in sync --- .../gms/location/manager/LocationManager.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt index fe4f476a6f..36d1af5a4f 100644 --- a/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt +++ b/play-services-location/core/src/main/kotlin/org/microg/gms/location/manager/LocationManager.kt @@ -13,6 +13,8 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.location.Location +import android.location.LocationManager.GPS_PROVIDER +import android.location.LocationManager.NETWORK_PROVIDER import android.os.* import android.os.Build.VERSION.SDK_INT import android.util.Log @@ -221,23 +223,33 @@ class LocationManager(private val context: Context, override val lifecycle: Life val locationManager = context.getSystemService() ?: return if (gpsInterval != currentGpsInterval) { - locationManager.requestSystemProviderUpdates(SystemLocationManager.GPS_PROVIDER, gpsInterval, QUALITY_HIGH_ACCURACY, gpsLocationListener) if (gpsInterval == Long.MAX_VALUE) { + // Fetch last location from GPS, just to make sure we already considered it try { - val newGpsLocation = locationManager.getLastKnownLocation(SystemLocationManager.GPS_PROVIDER) + val newGpsLocation = locationManager.getLastKnownLocation(GPS_PROVIDER) if (newGpsLocation != null && newGpsLocation.elapsedMillis > (lastGpsLocation?.elapsedMillis ?: 0)) { updateGpsLocation(newGpsLocation) } - currentGpsInterval = gpsInterval } catch (e: SecurityException) { // Ignore } } + try { + locationManager.requestSystemProviderUpdates(GPS_PROVIDER, gpsInterval, QUALITY_HIGH_ACCURACY, gpsLocationListener) + currentGpsInterval = gpsInterval + } catch (e: Exception) { + // Ignore + } } - if (!context.hasNetworkLocationServiceBuiltIn() && LocationManagerCompat.hasProvider(locationManager, SystemLocationManager.NETWORK_PROVIDER) && currentNetworkInterval != networkInterval) { + if (!context.hasNetworkLocationServiceBuiltIn() && LocationManagerCompat.hasProvider(locationManager, NETWORK_PROVIDER) && currentNetworkInterval != networkInterval) { boundToSystemNetworkLocation = true - locationManager.requestSystemProviderUpdates(SystemLocationManager.NETWORK_PROVIDER, networkInterval, if (lowPower) QUALITY_LOW_POWER else QUALITY_BALANCED_POWER_ACCURACY, networkLocationListener) - currentNetworkInterval = networkInterval + try { + val quality = if (lowPower) QUALITY_LOW_POWER else QUALITY_BALANCED_POWER_ACCURACY + locationManager.requestSystemProviderUpdates(NETWORK_PROVIDER, networkInterval, quality, networkLocationListener) + currentNetworkInterval = networkInterval + } catch (e: Exception) { + // Ignore + } } } @@ -246,7 +258,7 @@ class LocationManager(private val context: Context, override val lifecycle: Life if (interval != Long.MAX_VALUE) { Log.d(TAG, "Request updates for $provider at interval ${interval}ms") LocationManagerCompat.requestLocationUpdates(this, provider, Builder(interval).setQuality(quality).build(), listener, context.mainLooper) - } else if (BuildConfig.ALWAYS_LISTEN_GPS_PASSIVE && provider == SystemLocationManager.GPS_PROVIDER) { + } else if (BuildConfig.ALWAYS_LISTEN_GPS_PASSIVE && provider == GPS_PROVIDER) { Log.d(TAG, "Request updates for $provider passively") LocationManagerCompat.requestLocationUpdates(this, provider, Builder(PASSIVE_INTERVAL).setQuality(QUALITY_LOW_POWER).setMinUpdateIntervalMillis(MAX_FINE_UPDATE_INTERVAL).build(), listener, context.mainLooper) } else { @@ -254,9 +266,9 @@ class LocationManager(private val context: Context, override val lifecycle: Life LocationManagerCompat.removeUpdates(this, listener) } } catch (e: SecurityException) { - // Ignore + throw RuntimeException(e) } catch (e: Exception) { - // Ignore + throw RuntimeException(e) } } @@ -344,6 +356,7 @@ class LocationManager(private val context: Context, override val lifecycle: Life writer.println("Location availability: ${lastLocationCapsule.locationAvailability}") writer.println("Last coarse location: ${postProcessor.process(lastLocationCapsule.getLocation(GRANULARITY_COARSE, Long.MAX_VALUE), GRANULARITY_COARSE, true)}") writer.println("Last fine location: ${postProcessor.process(lastLocationCapsule.getLocation(GRANULARITY_FINE, Long.MAX_VALUE), GRANULARITY_FINE, true)}") + writer.println("Interval: gps=${if (currentGpsInterval==Long.MAX_VALUE) "off" else currentGpsInterval.formatDuration()} network=${if (currentNetworkInterval==Long.MAX_VALUE) "off" else currentNetworkInterval.formatDuration()}") writer.println("Network location: built-in=${context.hasNetworkLocationServiceBuiltIn()} system=$boundToSystemNetworkLocation") requestManager.dump(writer) deviceOrientationManager.dump(writer) From c5b73835209a41c5dc03963c49b93b9ac14d9d13 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 20 Sep 2023 17:38:39 +0200 Subject: [PATCH 15/59] Create Work Account service --- play-services-auth-workaccount/build.gradle | 42 ++++++++++++ .../core/build.gradle | 45 +++++++++++++ .../core/src/main/AndroidManifest.xml | 26 ++++++++ .../auth/workaccount/WorkAccountService.kt | 64 +++++++++++++++++++ .../src/main/AndroidManifest.xml | 6 ++ .../gms/auth/account/IWorkAccountService.aidl | 21 ++++++ .../org/microg/gms/common/GmsService.java | 2 +- play-services-core/build.gradle | 1 + .../src/main/AndroidManifest.xml | 11 +++- .../gms/auth/account/WorkAccountApi.java | 4 ++ settings.gradle | 2 + 11 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 play-services-auth-workaccount/build.gradle create mode 100644 play-services-auth-workaccount/core/build.gradle create mode 100644 play-services-auth-workaccount/core/src/main/AndroidManifest.xml create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt create mode 100644 play-services-auth-workaccount/src/main/AndroidManifest.xml create mode 100644 play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl create mode 100644 play-services-core/src/main/java/com/google/android/gms/auth/account/WorkAccountApi.java diff --git a/play-services-auth-workaccount/build.gradle b/play-services-auth-workaccount/build.gradle new file mode 100644 index 0000000000..0b0dfff78d --- /dev/null +++ b/play-services-auth-workaccount/build.gradle @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 e foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' +apply plugin: 'signing' + +android { + namespace "com.google.android.gms.auth.workaccount" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + buildFeatures { + aidl = true + } + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + +} + +apply from: '../gradle/publish-android.gradle' + +description = 'microG implementation of play-services-auth-api-phone' + +dependencies { + // Dependencies from play-services-auth-api-phone:18.0.1 + api project(':play-services-base') + api project(':play-services-basement') + api project(':play-services-tasks') +} diff --git a/play-services-auth-workaccount/core/build.gradle b/play-services-auth-workaccount/core/build.gradle new file mode 100644 index 0000000000..41a453103e --- /dev/null +++ b/play-services-auth-workaccount/core/build.gradle @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2023 e foundation + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +dependencies { + api project(':play-services-auth-workaccount') + api project(':play-services-auth') + implementation project(':play-services-base-core') + + implementation "androidx.appcompat:appcompat:$appcompatVersion" +} + +android { + namespace "com.google.android.gms.auth.workaccount" + + compileSdkVersion androidCompileSdk + buildToolsVersion "$androidBuildVersionTools" + + defaultConfig { + versionName version + minSdkVersion androidMinSdk + targetSdkVersion androidTargetSdk + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + + kotlinOptions { + jvmTarget = 1.8 + } + + lintOptions { + disable 'MissingTranslation' + } +} diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..55635a9464 --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt new file mode 100644 index 0000000000..0290a1f7ab --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -0,0 +1,64 @@ +package org.microg.gms.auth.workaccount + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Parcel +import android.util.Log +import com.google.android.gms.auth.account.IWorkAccountService +import com.google.android.gms.auth.account.IWorkAccountService.AddAccountResult +import com.google.android.gms.common.Feature +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.common.api.Status +import com.google.android.gms.common.internal.ConnectionInfo +import com.google.android.gms.common.internal.GetServiceRequest +import com.google.android.gms.common.internal.IGmsCallbacks +import com.google.android.gms.dynamic.IObjectWrapper +import com.google.android.gms.dynamic.ObjectWrapper +import com.google.android.gms.dynamic.unwrap +import org.microg.gms.BaseService +import org.microg.gms.common.GmsService + +private const val TAG = "GmsWorkAccountService" + +class WorkAccountService : BaseService(TAG, GmsService.WORK_ACCOUNT) { + override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { + callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, WorkAccountServiceImpl(this), ConnectionInfo().apply { + features = arrayOf(Feature("work_account_client_is_whitelisted", 1)) + } ) + } +} + +class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() { + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + Log.d(TAG, "$code, $data, $reply, $flags") + return super.onTransact(code, data, reply, flags) + } + + override fun addWorkAccount(googleApiClient: IObjectWrapper?, s: String?): IWorkAccountService.AddAccountResult { + Log.d(TAG, "addWorkAccount with $googleApiClient, $s") + return object : AddAccountResult.Stub() { + override fun getAccount(): Account? { + // TODO + return AccountManager.get(context).accounts.firstOrNull()?.also { Log.d(TAG, "returning account $it") } + } + + override fun getStatus(): IObjectWrapper { + return ObjectWrapper.wrap(Status(CommonStatusCodes.SUCCESS)).also { Log.d(TAG, "returning status $it (${it.unwrap()})") } + } + } + } + + override fun removeWorkAccount(googleApiClient: IObjectWrapper?, account: IObjectWrapper?): IObjectWrapper { + return ObjectWrapper.wrap(null) + } + + override fun setWorkAuthenticatorEnabled(googleApiClient: IObjectWrapper?, b: Boolean) { + // TODO + Log.d(TAG, "setWorkAuthenticatorEnabled with $googleApiClient, $b") + } + + override fun setWorkAuthenticatorEnabledWithResult(googleApiClient: IObjectWrapper?, b: Boolean): IObjectWrapper { + return ObjectWrapper.wrap(null) + } +} diff --git a/play-services-auth-workaccount/src/main/AndroidManifest.xml b/play-services-auth-workaccount/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..fe3a9e6548 --- /dev/null +++ b/play-services-auth-workaccount/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl b/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl new file mode 100644 index 0000000000..3108d7a0b9 --- /dev/null +++ b/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl @@ -0,0 +1,21 @@ +package com.google.android.gms.auth.account; + +import android.accounts.Account; +import com.google.android.gms.dynamic.IObjectWrapper; + +interface IWorkAccountService { + + interface AddAccountResult { + Account getAccount(); + IObjectWrapper getStatus(); + } + + void setWorkAuthenticatorEnabled(IObjectWrapper googleApiClient, boolean b); + + AddAccountResult addWorkAccount(IObjectWrapper googleApiClient, String s); + + IObjectWrapper removeWorkAccount(IObjectWrapper googleApiClient, IObjectWrapper account); + + + IObjectWrapper setWorkAuthenticatorEnabledWithResult(IObjectWrapper googleApiClient, boolean b); +} \ No newline at end of file diff --git a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java index 7493a9d03d..0eb2f055e4 100644 --- a/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java +++ b/play-services-basement/src/main/java/org/microg/gms/common/GmsService.java @@ -102,7 +102,7 @@ public enum GmsService { FIREBASE_AUTH(112, "com.google.firebase.auth.api.gms.service.START"), APP_INDEXING(113), GASS(116, "com.google.android.gms.gass.START"), - WORK_ACCOUNT(120), + WORK_ACCOUNT(120, "com.google.android.gms.auth.account.workaccount.START"), INSTANT_APPS(121, "com.google.android.gms.instantapps.START"), CAST_FIRSTPATY(122, "com.google.android.gms.cast.firstparty.START"), AD_CACHE(123, "com.google.android.gms.ads.service.CACHE"), diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index a2bdcd4a8d..701698c183 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation project(':play-services-appinvite-core') implementation project(':play-services-appset-core') implementation project(':play-services-auth-api-phone-core') + implementation project(':play-services-auth-workaccount-core') implementation project(':play-services-base-core') implementation project(':play-services-cast-core') implementation project(':play-services-cast-framework-core') diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index a882f9fc4a..cf38ec027c 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -18,7 +18,8 @@ + + + + + + + + diff --git a/play-services-core/src/main/java/com/google/android/gms/auth/account/WorkAccountApi.java b/play-services-core/src/main/java/com/google/android/gms/auth/account/WorkAccountApi.java new file mode 100644 index 0000000000..cdafb76e76 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/auth/account/WorkAccountApi.java @@ -0,0 +1,4 @@ +package com.google.android.gms.auth.account; + +public interface WorkAccountApi { +} diff --git a/settings.gradle b/settings.gradle index 0c2ee077ef..431bf5ac01 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,7 @@ include ':play-services-appinvite' include ':play-services-appset' include ':play-services-auth' include ':play-services-auth-api-phone' +include ':play-services-auth-workaccount' include ':play-services-auth-base' include ':play-services-base' include ':play-services-basement' @@ -80,6 +81,7 @@ sublude ':play-services-ads-lite:core' sublude ':play-services-appinvite:core' sublude ':play-services-appset:core' sublude ':play-services-auth-api-phone:core' +sublude ':play-services-auth-workaccount:core' sublude ':play-services-base:core' sublude ':play-services-base:core:package' sublude ':play-services-cast:core' From 686ab6d06186059a9e827f92da20c1850d8d1f38 Mon Sep 17 00:00:00 2001 From: Jonathan Klee Date: Wed, 14 Aug 2024 15:06:59 +0200 Subject: [PATCH 16/59] Introduce WorkAccountAuthenticatorService --- .../core/src/main/AndroidManifest.xml | 20 +++++++++++++++++++ .../WorkAccountAuthenticatorService.kt | 12 +++++++++++ .../auth/workaccount/WorkAccountService.kt | 14 +++++++++++++ .../src/main/res/drawable/ic_google_logo.xml | 19 ++++++++++++++++++ .../core/src/main/res/values/string.xml | 6 ++++++ .../main/res/xml/auth_work_authenticator.xml | 7 +++++++ 6 files changed, 78 insertions(+) create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt create mode 100644 play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml create mode 100644 play-services-auth-workaccount/core/src/main/res/values/string.xml create mode 100644 play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml index 55635a9464..93aeb14cd5 100644 --- a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -22,5 +22,25 @@ + + + + + + + + + + + + diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt new file mode 100644 index 0000000000..6cf0c8f4b0 --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt @@ -0,0 +1,12 @@ +package org.microg.gms.auth.account.authenticator + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class WorkAccountAuthenticatorService : Service() { + + override fun onBind(intent: Intent?): IBinder? { + return null + } +} \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 0290a1f7ab..4299ccee87 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -2,8 +2,12 @@ package org.microg.gms.auth.workaccount import android.accounts.Account import android.accounts.AccountManager +import android.app.admin.DevicePolicyManager +import android.content.ComponentName import android.content.Context +import android.content.pm.PackageManager import android.os.Parcel +import android.os.UserManager import android.util.Log import com.google.android.gms.auth.account.IWorkAccountService import com.google.android.gms.auth.account.IWorkAccountService.AddAccountResult @@ -40,6 +44,7 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() return object : AddAccountResult.Stub() { override fun getAccount(): Account? { // TODO + return AccountManager.get(context).accounts.firstOrNull()?.also { Log.d(TAG, "returning account $it") } } @@ -50,15 +55,24 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() } override fun removeWorkAccount(googleApiClient: IObjectWrapper?, account: IObjectWrapper?): IObjectWrapper { + Log.d(TAG, "removeWorkAccount") return ObjectWrapper.wrap(null) } override fun setWorkAuthenticatorEnabled(googleApiClient: IObjectWrapper?, b: Boolean) { // TODO Log.d(TAG, "setWorkAuthenticatorEnabled with $googleApiClient, $b") + val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val userManger = context.getSystemService(Context.USER_SERVICE) as UserManager + val sharedPreferences = context.getSharedPreferences("work_account_prefs", Context.MODE_PRIVATE) + sharedPreferences.edit().putBoolean("enabled_by_admin", true).apply() + + val componentName = ComponentName("com.google.android.gms", "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService") + //context.packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) } override fun setWorkAuthenticatorEnabledWithResult(googleApiClient: IObjectWrapper?, b: Boolean): IObjectWrapper { + Log.d(TAG, "setWorkAuthenticatorEnabledWithResult $googleApiClient, $b") return ObjectWrapper.wrap(null) } } diff --git a/play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml b/play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml new file mode 100644 index 0000000000..c6c1b58b2d --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/res/values/string.xml b/play-services-auth-workaccount/core/src/main/res/values/string.xml new file mode 100644 index 0000000000..0ccd65cf18 --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/res/values/string.xml @@ -0,0 +1,6 @@ + + + + Yo babe + + diff --git a/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml b/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml new file mode 100644 index 0000000000..417255936d --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml @@ -0,0 +1,7 @@ + + From 96e6b0c5b4ce9ea1acc0b03c3e51fa2b5a4fae45 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 27 Aug 2024 22:57:33 +0200 Subject: [PATCH 17/59] Work account POC --- .../core/src/main/AndroidManifest.xml | 5 +- .../authenticator/WorkAccountAuthenticator.kt | 84 +++++++++++++++++++ .../WorkAccountAuthenticatorService.kt | 17 ++++ .../WorkAccountAuthenticatorService.kt | 12 --- .../auth/workaccount/WorkAccountService.kt | 8 +- .../src/main/res/drawable/ic_briefcase.xml | 5 ++ .../src/main/res/drawable/ic_google_logo.xml | 19 ----- .../core/src/main/res/values/string.xml | 2 +- .../main/res/xml/auth_work_authenticator.xml | 4 +- 9 files changed, 118 insertions(+), 38 deletions(-) create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt delete mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt create mode 100644 play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml delete mode 100644 play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml index 93aeb14cd5..b83ee87d48 100644 --- a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + @@ -23,7 +26,7 @@ diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt new file mode 100644 index 0000000000..f5ba093eb0 --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -0,0 +1,84 @@ +package com.google.android.gms.auth.account.authenticator + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.content.Context +import android.os.Bundle +import android.util.Log + +class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { + + override fun editProperties( + response: AccountAuthenticatorResponse, + accountType: String? + ): Bundle { + TODO("Not yet implemented: editProperties") + } + + override fun addAccount( + response: AccountAuthenticatorResponse, + accountType: String, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle + ): Bundle { + val name = "account${options.getInt(AccountManager.KEY_CALLER_UID)}" + val type = "com.google.work" + AccountManager.get(context).addAccountExplicitly( + Account(name, type), "***", Bundle() + ) + return Bundle().apply { + putString(AccountManager.KEY_ACCOUNT_NAME, name) + putString(AccountManager.KEY_ACCOUNT_TYPE, type) + } + } + + override fun confirmCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + options: Bundle? + ): Bundle { + return Bundle().apply { + putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true) + } + } + + override fun getAuthToken( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ): Bundle { + TODO("Not yet implemented: getAuthToken") + } + + override fun getAuthTokenLabel(authTokenType: String?): String { + TODO("Not yet implemented: getAuthTokenLabel") + } + + override fun updateCredentials( + response: AccountAuthenticatorResponse?, + account: Account?, + authTokenType: String?, + options: Bundle? + ): Bundle { + TODO("Not yet implemented: updateCredentials") + } + + override fun hasFeatures( + response: AccountAuthenticatorResponse?, + account: Account?, + features: Array + ): Bundle { + Log.i(TAG, "Queried features: " + features.joinToString(", ")) + return Bundle().apply { + putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false) + } + } + + companion object { + const val TAG = "WorkAccAuthenticator" + } +} \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt new file mode 100644 index 0000000000..1795fa33d0 --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt @@ -0,0 +1,17 @@ +package com.google.android.gms.auth.account.authenticator + +import android.accounts.AccountManager +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class WorkAccountAuthenticatorService : Service() { + private val authenticator by lazy { WorkAccountAuthenticator(this) } + + override fun onBind(intent: Intent): IBinder? { + if (intent.action == AccountManager.ACTION_AUTHENTICATOR_INTENT) { + return authenticator.iBinder + } + return null + } +} \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt deleted file mode 100644 index 6cf0c8f4b0..0000000000 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt +++ /dev/null @@ -1,12 +0,0 @@ -package org.microg.gms.auth.account.authenticator - -import android.app.Service -import android.content.Intent -import android.os.IBinder - -class WorkAccountAuthenticatorService : Service() { - - override fun onBind(intent: Intent?): IBinder? { - return null - } -} \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 4299ccee87..2144c79094 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -39,11 +39,13 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() return super.onTransact(code, data, reply, flags) } - override fun addWorkAccount(googleApiClient: IObjectWrapper?, s: String?): IWorkAccountService.AddAccountResult { + override fun addWorkAccount(googleApiClient: IObjectWrapper?, s: String?): AddAccountResult { + // TODO: caller expects that an account is actually created Log.d(TAG, "addWorkAccount with $googleApiClient, $s") + Log.d(TAG, "stub implementation, not creating account; please create manually!") + // TODO: use correct AIDL return object : AddAccountResult.Stub() { override fun getAccount(): Account? { - // TODO return AccountManager.get(context).accounts.firstOrNull()?.also { Log.d(TAG, "returning account $it") } } @@ -68,7 +70,7 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() sharedPreferences.edit().putBoolean("enabled_by_admin", true).apply() val componentName = ComponentName("com.google.android.gms", "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService") - //context.packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) + context.packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) } override fun setWorkAuthenticatorEnabledWithResult(googleApiClient: IObjectWrapper?, b: Boolean): IObjectWrapper { diff --git a/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml b/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml new file mode 100644 index 0000000000..2f9846c48b --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml b/play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml deleted file mode 100644 index c6c1b58b2d..0000000000 --- a/play-services-auth-workaccount/core/src/main/res/drawable/ic_google_logo.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/res/values/string.xml b/play-services-auth-workaccount/core/src/main/res/values/string.xml index 0ccd65cf18..2160b03a07 100644 --- a/play-services-auth-workaccount/core/src/main/res/values/string.xml +++ b/play-services-auth-workaccount/core/src/main/res/values/string.xml @@ -1,6 +1,6 @@ - Yo babe + Mock account for Device Policy Controller diff --git a/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml b/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml index 417255936d..e0a1155ff7 100644 --- a/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml +++ b/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml @@ -1,7 +1,7 @@ From 7f4f2528e968e0a122908dc5064f1cd2570862d3 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 28 Aug 2024 00:08:26 +0200 Subject: [PATCH 18/59] Work profile: use correct AIDL --- .../core/src/main/AndroidManifest.xml | 8 +-- .../auth/workaccount/WorkAccountService.kt | 67 ++++++++----------- .../auth/account/IWorkAccountCallback.aidl | 8 +++ .../gms/auth/account/IWorkAccountService.aidl | 16 ++--- 4 files changed, 42 insertions(+), 57 deletions(-) create mode 100644 play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountCallback.aidl diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml index b83ee87d48..dd5f602a9a 100644 --- a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -18,13 +18,7 @@ - - - - - - + android:exported="true" /> ()})") } - } - } + val componentName = ComponentName("com.google.android.gms", "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService") + context.packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) } - override fun removeWorkAccount(googleApiClient: IObjectWrapper?, account: IObjectWrapper?): IObjectWrapper { - Log.d(TAG, "removeWorkAccount") - return ObjectWrapper.wrap(null) + override fun addWorkAccount( + callback: IWorkAccountCallback?, + token: String? + ) { + // TODO: caller expects that an account is actually created + Log.d(TAG, "addWorkAccount with token $token") + Log.d(TAG, "stub implementation, not creating account; please create manually!") + // if account already exists, return one of those as the "new one" (temporary implementation) + AccountManager.get(context).getAccountsByType("com.google.work").firstOrNull()?.let { + callback?.onAccountAdded(it) + } } - override fun setWorkAuthenticatorEnabled(googleApiClient: IObjectWrapper?, b: Boolean) { - // TODO - Log.d(TAG, "setWorkAuthenticatorEnabled with $googleApiClient, $b") - val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val userManger = context.getSystemService(Context.USER_SERVICE) as UserManager - val sharedPreferences = context.getSharedPreferences("work_account_prefs", Context.MODE_PRIVATE) - sharedPreferences.edit().putBoolean("enabled_by_admin", true).apply() - - val componentName = ComponentName("com.google.android.gms", "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService") - context.packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) - } + override fun removeWorkAccount( + callback: IWorkAccountCallback?, + account: Account? + ) { + // TODO: caller expects that the account is actually removed + Log.d(TAG, "removeWorkAccount with account ${account?.name}") + Log.d(TAG, "stub implementation, not removing account; please remove manually!") + val noAccounts = AccountManager.get(context).getAccountsByType("com.google.work").isEmpty() + callback?.onAccountRemoved(noAccounts) - override fun setWorkAuthenticatorEnabledWithResult(googleApiClient: IObjectWrapper?, b: Boolean): IObjectWrapper { - Log.d(TAG, "setWorkAuthenticatorEnabledWithResult $googleApiClient, $b") - return ObjectWrapper.wrap(null) } } diff --git a/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountCallback.aidl b/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountCallback.aidl new file mode 100644 index 0000000000..e07bf9c091 --- /dev/null +++ b/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountCallback.aidl @@ -0,0 +1,8 @@ +package com.google.android.gms.auth.account; + +import android.accounts.Account; + +interface IWorkAccountCallback { + void onAccountAdded(in Account account) = 0; + void onAccountRemoved(boolean success) = 1; +} \ No newline at end of file diff --git a/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl b/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl index 3108d7a0b9..1e773a2bad 100644 --- a/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl +++ b/play-services-auth-workaccount/src/main/aidl/com/google/android/gms/auth/account/IWorkAccountService.aidl @@ -1,21 +1,13 @@ package com.google.android.gms.auth.account; import android.accounts.Account; -import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.auth.account.IWorkAccountCallback; interface IWorkAccountService { - interface AddAccountResult { - Account getAccount(); - IObjectWrapper getStatus(); - } + void setWorkAuthenticatorEnabled(boolean enabled) = 0; - void setWorkAuthenticatorEnabled(IObjectWrapper googleApiClient, boolean b); + void addWorkAccount(IWorkAccountCallback callback, String token) = 1; - AddAccountResult addWorkAccount(IObjectWrapper googleApiClient, String s); - - IObjectWrapper removeWorkAccount(IObjectWrapper googleApiClient, IObjectWrapper account); - - - IObjectWrapper setWorkAuthenticatorEnabledWithResult(IObjectWrapper googleApiClient, boolean b); + void removeWorkAccount(IWorkAccountCallback callback, in Account account) = 2; } \ No newline at end of file From 762fa5325a979d968622e45281a6169e800a22ff Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 1 Sep 2024 18:34:23 +0200 Subject: [PATCH 19/59] Call account creation when app is using API to create work account --- .../core/src/main/AndroidManifest.xml | 3 + .../authenticator/WorkAccountAuthenticator.kt | 11 +++- .../auth/workaccount/WorkAccountService.kt | 62 +++++++++++++++---- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml index dd5f602a9a..cea47de20e 100644 --- a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -12,6 +12,9 @@ + diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index f5ba093eb0..c9b9d94e2e 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -25,13 +25,16 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica options: Bundle ): Bundle { val name = "account${options.getInt(AccountManager.KEY_CALLER_UID)}" - val type = "com.google.work" + val password = if (options.containsKey(KEY_ACCOUNT_CREATION_TOKEN)) { + options.getString(KEY_ACCOUNT_CREATION_TOKEN).also { Log.d(TAG, "read token $it") } + } else null + AccountManager.get(context).addAccountExplicitly( - Account(name, type), "***", Bundle() + Account(name, WORK_ACCOUNT_TYPE), password, Bundle() ) return Bundle().apply { putString(AccountManager.KEY_ACCOUNT_NAME, name) - putString(AccountManager.KEY_ACCOUNT_TYPE, type) + putString(AccountManager.KEY_ACCOUNT_TYPE, WORK_ACCOUNT_TYPE) } } @@ -80,5 +83,7 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica companion object { const val TAG = "WorkAccAuthenticator" + const val WORK_ACCOUNT_TYPE = "com.google.work" + const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken" } } \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 1be029f8da..0053b902f8 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -5,10 +5,13 @@ import android.accounts.AccountManager import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager +import android.os.Bundle import android.os.Parcel import android.util.Log import com.google.android.gms.auth.account.IWorkAccountCallback import com.google.android.gms.auth.account.IWorkAccountService +import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.KEY_ACCOUNT_CREATION_TOKEN +import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_TYPE import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.internal.ConnectionInfo @@ -20,14 +23,25 @@ import org.microg.gms.common.GmsService private const val TAG = "GmsWorkAccountService" class WorkAccountService : BaseService(TAG, GmsService.WORK_ACCOUNT) { - override fun handleServiceRequest(callback: IGmsCallbacks, request: GetServiceRequest, service: GmsService) { - callback.onPostInitCompleteWithConnectionInfo(CommonStatusCodes.SUCCESS, WorkAccountServiceImpl(this), ConnectionInfo().apply { - features = arrayOf(Feature("work_account_client_is_whitelisted", 1)) - } ) + override fun handleServiceRequest( + callback: IGmsCallbacks, + request: GetServiceRequest, + service: GmsService + ) { + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + WorkAccountServiceImpl(this), + ConnectionInfo().apply { + features = arrayOf(Feature("work_account_client_is_whitelisted", 1)) + }) } } -class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() { +class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { + + val packageManager: PackageManager = context.packageManager + val accountManager: AccountManager = AccountManager.get(context) + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { Log.d(TAG, "$code, $data, $reply, $flags") return super.onTransact(code, data, reply, flags) @@ -40,20 +54,42 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() val sharedPreferences = context.getSharedPreferences("work_account_prefs", Context.MODE_PRIVATE) sharedPreferences.edit().putBoolean("enabled_by_admin", true).apply()*/ - val componentName = ComponentName("com.google.android.gms", "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService") - context.packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) + val componentName = ComponentName( + "com.google.android.gms", + "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService" + ) + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) } override fun addWorkAccount( callback: IWorkAccountCallback?, token: String? ) { - // TODO: caller expects that an account is actually created Log.d(TAG, "addWorkAccount with token $token") - Log.d(TAG, "stub implementation, not creating account; please create manually!") - // if account already exists, return one of those as the "new one" (temporary implementation) - AccountManager.get(context).getAccountsByType("com.google.work").firstOrNull()?.let { - callback?.onAccountAdded(it) + val future = accountManager.addAccount( + WORK_ACCOUNT_TYPE, + null, + null, + Bundle().apply { putString(KEY_ACCOUNT_CREATION_TOKEN, token) }, + null, + null, + null + ) + callback?.let { + Thread { + future.result.let { result -> + it.onAccountAdded( + Account( + result.getString(AccountManager.KEY_ACCOUNT_NAME), + result.getString(AccountManager.KEY_ACCOUNT_TYPE) + ) + ) + } + }.start() } } @@ -64,7 +100,7 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() // TODO: caller expects that the account is actually removed Log.d(TAG, "removeWorkAccount with account ${account?.name}") Log.d(TAG, "stub implementation, not removing account; please remove manually!") - val noAccounts = AccountManager.get(context).getAccountsByType("com.google.work").isEmpty() + val noAccounts = accountManager.getAccountsByType("com.google.work").isEmpty() callback?.onAccountRemoved(noAccounts) } From 999e1d31ef30bfa3aac9e92589a438c8577f928c Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 1 Sep 2024 20:00:06 +0200 Subject: [PATCH 20/59] Work account: Fix rough edges --- .../authenticator/WorkAccountAuthenticator.kt | 36 ++++++++++++++-- .../auth/workaccount/WorkAccountService.kt | 43 ++++++++++--------- .../core/src/main/res/values/string.xml | 1 + 3 files changed, 57 insertions(+), 23 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index c9b9d94e2e..912c6c5c97 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -5,8 +5,10 @@ import android.accounts.Account import android.accounts.AccountAuthenticatorResponse import android.accounts.AccountManager import android.content.Context +import android.os.Build import android.os.Bundle import android.util.Log +import com.google.android.gms.auth.workaccount.R class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { @@ -24,10 +26,21 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica requiredFeatures: Array?, options: Bundle ): Bundle { + if (!options.containsKey(KEY_ACCOUNT_CREATION_TOKEN)) { + Log.w(TAG, "refusing to add account without creation token: was likely manually initiated by user") + + // TODO: The error message is not automatically displayed by the settings app as of now. + // We can consider showing the error message through a popup instead. + + return Bundle().apply { + putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) + putString(AccountManager.KEY_ERROR_MESSAGE, + context.getString(R.string.auth_work_authenticator_add_manual_error) + ) + } + } val name = "account${options.getInt(AccountManager.KEY_CALLER_UID)}" - val password = if (options.containsKey(KEY_ACCOUNT_CREATION_TOKEN)) { - options.getString(KEY_ACCOUNT_CREATION_TOKEN).also { Log.d(TAG, "read token $it") } - } else null + val password = options.getString(KEY_ACCOUNT_CREATION_TOKEN).also { Log.d(TAG, "read token $it") } AccountManager.get(context).addAccountExplicitly( Account(name, WORK_ACCOUNT_TYPE), password, Bundle() @@ -81,6 +94,23 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica } } + /** + * Prevent accidental deletion, unlike GMS. The account can only be removed through client apps; + * ideally, it would only be removed by the app that requested it to be created / the DPC + * manager, though this is not enforced. On API 21, the account can also be removed by hand + * because `removeAccountExplicitly` is not available on API 21. + */ + override fun getAccountRemovalAllowed( + response: AccountAuthenticatorResponse?, + account: Account? + ): Bundle { + return Bundle().apply { + putBoolean(AccountManager.KEY_BOOLEAN_RESULT, + Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1 + ) + } + } + companion object { const val TAG = "WorkAccAuthenticator" const val WORK_ACCOUNT_TYPE = "com.google.work" diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 0053b902f8..4f416dbbeb 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -5,6 +5,7 @@ import android.accounts.AccountManager import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager +import android.os.Build import android.os.Bundle import android.os.Parcel import android.util.Log @@ -49,10 +50,6 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { override fun setWorkAuthenticatorEnabled(enabled: Boolean) { Log.d(TAG, "setWorkAuthenticatorEnabled with $enabled") - /*val devicePolicyManager = context.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val userManger = context.getSystemService(Context.USER_SERVICE) as UserManager - val sharedPreferences = context.getSharedPreferences("work_account_prefs", Context.MODE_PRIVATE) - sharedPreferences.edit().putBoolean("enabled_by_admin", true).apply()*/ val componentName = ComponentName( "com.google.android.gms", @@ -79,29 +76,35 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { null, null ) - callback?.let { - Thread { - future.result.let { result -> - it.onAccountAdded( - Account( - result.getString(AccountManager.KEY_ACCOUNT_NAME), - result.getString(AccountManager.KEY_ACCOUNT_TYPE) - ) + Thread { + future.result.let { result -> + callback?.onAccountAdded( + Account( + result.getString(AccountManager.KEY_ACCOUNT_NAME), + result.getString(AccountManager.KEY_ACCOUNT_TYPE) ) - } - }.start() - } + ) + } + }.start() } override fun removeWorkAccount( callback: IWorkAccountCallback?, account: Account? ) { - // TODO: caller expects that the account is actually removed Log.d(TAG, "removeWorkAccount with account ${account?.name}") - Log.d(TAG, "stub implementation, not removing account; please remove manually!") - val noAccounts = accountManager.getAccountsByType("com.google.work").isEmpty() - callback?.onAccountRemoved(noAccounts) - + account?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + val success = accountManager.removeAccountExplicitly(it) + callback?.onAccountRemoved(success) + } else { + val future = accountManager.removeAccount(it, null, null) + Thread { + future.result.let { result -> + callback?.onAccountRemoved(result) + } + }.start() + } + } } } diff --git a/play-services-auth-workaccount/core/src/main/res/values/string.xml b/play-services-auth-workaccount/core/src/main/res/values/string.xml index 2160b03a07..7ccab49ede 100644 --- a/play-services-auth-workaccount/core/src/main/res/values/string.xml +++ b/play-services-auth-workaccount/core/src/main/res/values/string.xml @@ -2,5 +2,6 @@ Mock account for Device Policy Controller + This type of account is not needed for device functionality and cannot be created manually. It may be created automatically by Device Policy Controller apps if needed. From 3af3991e28255d1dc4fe1cedab471c19fd6739b7 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 1 Sep 2024 21:01:11 +0200 Subject: [PATCH 21/59] Work account: Add security checks Verify that work accounts are only added by device owners or profile owners. For instance, Microsoft Intune will create a work profile (moving itself to the work profile in the process) before using the work account servce to create a work account, so at that point it will already be profile owner. Apps that are not the profile owner will subsequently not be able to disable the work account authenticator or remove the work account. The personal account would not have an owner and thus no application could enable the work account provider there. --- .../auth/workaccount/WorkAccountService.kt | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 4f416dbbeb..2de14e1a56 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -2,6 +2,7 @@ package org.microg.gms.auth.workaccount import android.accounts.Account import android.accounts.AccountManager +import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager @@ -20,6 +21,7 @@ import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService import org.microg.gms.common.GmsService +import org.microg.gms.common.PackageUtils private const val TAG = "GmsWorkAccountService" @@ -29,12 +31,26 @@ class WorkAccountService : BaseService(TAG, GmsService.WORK_ACCOUNT) { request: GetServiceRequest, service: GmsService ) { - callback.onPostInitCompleteWithConnectionInfo( - CommonStatusCodes.SUCCESS, - WorkAccountServiceImpl(this), - ConnectionInfo().apply { - features = arrayOf(Feature("work_account_client_is_whitelisted", 1)) - }) + val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) + val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val authorized = policyManager.isDeviceOwnerApp(packageName) || policyManager.isProfileOwnerApp(packageName) + + if (authorized) { + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + WorkAccountServiceImpl(this), + ConnectionInfo().apply { + features = arrayOf(Feature("work_account_client_is_whitelisted", 1)) + }) + } else { + // Return mock response, don't tell client that it is whitelisted + callback.onPostInitCompleteWithConnectionInfo( + CommonStatusCodes.SUCCESS, + UnauthorizedWorkAccountServiceImpl(), + ConnectionInfo().apply { + features = emptyArray() + }) + } } } @@ -67,6 +83,9 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { token: String? ) { Log.d(TAG, "addWorkAccount with token $token") + if (accountManager.getAccountsByType(WORK_ACCOUNT_TYPE).isNotEmpty()) { + Log.w(TAG, "A work account already exists. Refusing to add a second one.") + } val future = accountManager.addAccount( WORK_ACCOUNT_TYPE, null, @@ -108,3 +127,17 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { } } } + +class UnauthorizedWorkAccountServiceImpl : IWorkAccountService.Stub() { + override fun setWorkAuthenticatorEnabled(enabled: Boolean) { + throw SecurityException("client not admin, yet tried to enable work authenticator") + } + + override fun addWorkAccount(callback: IWorkAccountCallback?, token: String?) { + throw SecurityException("client not admin, yet tried to add work account") + } + + override fun removeWorkAccount(callback: IWorkAccountCallback?, account: Account?) { + throw SecurityException("client not admin, yet tried to remove work account") + } +} \ No newline at end of file From 5e2c9d3925e1c8722d675a2ac295184bbc75d7d4 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 2 Sep 2024 15:35:31 +0200 Subject: [PATCH 22/59] Work account: Redeem token when signing in to account --- .../authenticator/WorkAccountAuthenticator.kt | 75 ++++- .../gms/auth/workaccount/AuthRequest.java | 262 ++++++++++++++++++ .../gms/auth/workaccount/AuthResponse.java | 134 +++++++++ .../auth/workaccount/WorkAccountService.kt | 7 +- .../core/src/main/res/values/string.xml | 4 +- .../org/microg/gms/common/HttpFormClient.java | 13 +- 6 files changed, 468 insertions(+), 27 deletions(-) create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 912c6c5c97..0659669788 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -9,6 +9,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import com.google.android.gms.auth.workaccount.R +import org.microg.gms.auth.workaccount.AuthRequest class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { @@ -25,30 +26,75 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica authTokenType: String?, requiredFeatures: Array?, options: Bundle - ): Bundle { - if (!options.containsKey(KEY_ACCOUNT_CREATION_TOKEN)) { - Log.w(TAG, "refusing to add account without creation token: was likely manually initiated by user") + ): Bundle? { + if ( + !options.containsKey(KEY_ACCOUNT_CREATION_TOKEN) + || options.getString(KEY_ACCOUNT_CREATION_TOKEN) == null + || options.getInt(AccountManager.KEY_CALLER_UID) != android.os.Process.myUid()) { + Log.e(TAG, + "refusing to add account without creation token or from external app: " + + "could have been manually initiated by user (not supported) " + + "or by unauthorized app (not allowed)" + ) // TODO: The error message is not automatically displayed by the settings app as of now. // We can consider showing the error message through a popup instead. return Bundle().apply { putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION) - putString(AccountManager.KEY_ERROR_MESSAGE, - context.getString(R.string.auth_work_authenticator_add_manual_error) + putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_add_manual_error) ) } } - val name = "account${options.getInt(AccountManager.KEY_CALLER_UID)}" - val password = options.getString(KEY_ACCOUNT_CREATION_TOKEN).also { Log.d(TAG, "read token $it") } - AccountManager.get(context).addAccountExplicitly( - Account(name, WORK_ACCOUNT_TYPE), password, Bundle() - ) - return Bundle().apply { - putString(AccountManager.KEY_ACCOUNT_NAME, name) - putString(AccountManager.KEY_ACCOUNT_TYPE, WORK_ACCOUNT_TYPE) + val oauthToken: String = options.getString(KEY_ACCOUNT_CREATION_TOKEN)!! + + try { + val authResponse = AuthRequest().fromContext(context) + .appIsGms() + .callerIsGms() + .service("ac2dm") + .token(oauthToken).isAccessToken() + .addAccount() + .getAccountId() + .droidguardResults(null) + .response + + val accountManager = AccountManager.get(context) + if (accountManager.addAccountExplicitly( + Account(authResponse.email, WORK_ACCOUNT_TYPE), + authResponse.token, Bundle().apply { + // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. + if (authResponse.accountId.isNotBlank()) { + putString(KEY_GOOGLE_USER_ID, authResponse.accountId) + } + putString(KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) + putString(KEY_ACCOUNT_SERVICES, authResponse.services) // expected to be "android" + } + )) { + // Report successful creation to caller + response.onResult(Bundle().apply { + putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email) + putString(AccountManager.KEY_ACCOUNT_TYPE, WORK_ACCOUNT_TYPE) + }) + } + + } catch (exception: Exception) { + response.onResult(Bundle().apply { + putInt( + AccountManager.KEY_ERROR_CODE, + AccountManager.ERROR_CODE_NETWORK_ERROR + ) + putString(AccountManager.KEY_ERROR_MESSAGE, exception.message) + }) } + + /* Note: as is not documented, `null` must only be returned after `response.onResult` was + * already called, hence forcing the requests to be synchronous. They are still async to + * the caller's main thread because AccountManager forces potentially blocking operations, + * like waiting for a response upon `addAccount`, not to be on the main thread. + */ + return null } override fun confirmCredentials( @@ -115,5 +161,8 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica const val TAG = "WorkAccAuthenticator" const val WORK_ACCOUNT_TYPE = "com.google.work" const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken" + private const val KEY_GOOGLE_USER_ID = "GoogleUserId" // TODO: use AuthConstants + private const val KEY_ACCOUNT_SERVICES = "services" // TODO: use AuthConstants + private const val KEY_ACCOUNT_CAPABILITIES = "capabilities" } } \ No newline at end of file diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java new file mode 100644 index 0000000000..7c9c3f577e --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: 2023 microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ +// TODO: deduplicate file +package org.microg.gms.auth.workaccount; + +import static org.microg.gms.common.HttpFormClient.RequestContent; +import static org.microg.gms.common.HttpFormClient.RequestHeader; + +import android.content.Context; + +import org.microg.gms.common.Constants; +import org.microg.gms.common.HttpFormClient; +import org.microg.gms.common.Utils; +import org.microg.gms.profile.Build; +import org.microg.gms.profile.ProfileManager; + +import java.io.IOException; +import java.util.Locale; +import java.util.Map; + +public class AuthRequest extends HttpFormClient.Request { + private static final String SERVICE_URL = "https://android.googleapis.com/auth"; + private static final String USER_AGENT = "GoogleAuth/1.4 (%s %s); gzip"; + + @RequestHeader("User-Agent") + private String userAgent; + + @RequestHeader("app") + @RequestContent("app") + public String app; + @RequestContent("client_sig") + public String appSignature; + @RequestContent("callerPkg") + public String caller; + @RequestContent("callerSig") + public String callerSignature; + @RequestHeader(value = "device", nullPresent = true) + @RequestContent(value = "androidId", nullPresent = true) + public String androidIdHex; + @RequestContent("sdk_version") + public int sdkVersion; + @RequestContent("device_country") + public String countryCode; + @RequestContent("operatorCountry") + public String operatorCountryCode; + @RequestContent("lang") + public String locale; + @RequestContent("google_play_services_version") + public int gmsVersion = Constants.GMS_VERSION_CODE; + @RequestContent("accountType") + public String accountType; + @RequestContent("Email") + public String email; + @RequestContent("service") + public String service; + @RequestContent("source") + public String source; + @RequestContent({"is_called_from_account_manager", "_opt_is_called_from_account_manager"}) + public boolean isCalledFromAccountManager; + @RequestContent("Token") + public String token; + @RequestContent("system_partition") + public boolean systemPartition; + @RequestContent("get_accountid") + public boolean getAccountId; + @RequestContent("ACCESS_TOKEN") + public boolean isAccessToken; + @RequestContent("droidguard_results") + public String droidguardResults; + @RequestContent("has_permission") + public boolean hasPermission; + @RequestContent("add_account") + public boolean addAccount; + @RequestContent("delegation_type") + public String delegationType; + @RequestContent("delegatee_user_id") + public String delegateeUserId; + @RequestContent("oauth2_foreground") + public String oauth2Foreground; + @RequestContent("token_request_options") + public String tokenRequestOptions; + @RequestContent("it_caveat_types") + public String itCaveatTypes; + @RequestContent("check_email") + public boolean checkEmail; + @RequestContent("request_visible_actions") + public String requestVisibleActions; + @RequestContent("oauth2_prompt") + public String oauth2Prompt; + @RequestContent("oauth2_include_profile") + public String oauth2IncludeProfile; + @RequestContent("oauth2_include_email") + public String oauth2IncludeEmail; + @HttpFormClient.RequestContentDynamic + public Map dynamicFields; + + public String deviceName; + public String buildVersion; + + @Override + protected void prepare() { + userAgent = String.format(USER_AGENT, deviceName, buildVersion); + } + + public AuthRequest build(Context context) { + ProfileManager.ensureInitialized(context); + sdkVersion = Build.VERSION.SDK_INT; + deviceName = Build.DEVICE; + buildVersion = Build.ID; + return this; + } + + public AuthRequest source(String source) { + this.source = source; + return this; + } + + public AuthRequest locale(Locale locale) { + this.locale = locale.toString(); + this.countryCode = locale.getCountry().toLowerCase(); + this.operatorCountryCode = locale.getCountry().toLowerCase(); + return this; + } + + public AuthRequest fromContext(Context context) { + build(context); + locale(Utils.getLocale(context)); + if (false) { + //androidIdHex = Long.toHexString(LastCheckinInfo.read(context).getAndroidId()); + } + if (false) { + deviceName = ""; + buildVersion = ""; + } + return this; + } + + public AuthRequest email(String email) { + this.email = email; + return this; + } + + public AuthRequest token(String token) { + this.token = token; + return this; + } + + public AuthRequest service(String service) { + this.service = service; + return this; + } + + public AuthRequest app(String app, String appSignature) { + this.app = app; + this.appSignature = appSignature; + return this; + } + + public AuthRequest appIsGms() { + return app(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1); + } + + public AuthRequest callerIsGms() { + return caller(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1); + } + + public AuthRequest callerIsApp() { + return caller(app, appSignature); + } + + public AuthRequest caller(String caller, String callerSignature) { + this.caller = caller; + this.callerSignature = callerSignature; + return this; + } + + public AuthRequest calledFromAccountManager() { + isCalledFromAccountManager = true; + return this; + } + + public AuthRequest addAccount() { + addAccount = true; + return this; + } + + public AuthRequest systemPartition(boolean systemPartition) { + this.systemPartition = systemPartition; + return this; + } + + public AuthRequest hasPermission(boolean hasPermission) { + this.hasPermission = hasPermission; + return this; + } + + public AuthRequest getAccountId() { + getAccountId = true; + return this; + } + + public AuthRequest isAccessToken() { + isAccessToken = true; + return this; + } + + public AuthRequest droidguardResults(String droidguardResults) { + this.droidguardResults = droidguardResults; + return this; + } + + public AuthRequest delegation(int delegationType, String delegateeUserId) { + this.delegationType = delegationType == 0 ? null : Integer.toString(delegationType); + this.delegateeUserId = delegateeUserId; + return this; + } + + public AuthRequest oauth2Foreground(String oauth2Foreground) { + this.oauth2Foreground = oauth2Foreground; + return this; + } + + public AuthRequest tokenRequestOptions(String tokenRequestOptions) { + this.tokenRequestOptions = tokenRequestOptions; + return this; + } + + public AuthRequest oauth2IncludeProfile(String oauth2IncludeProfile) { + this.oauth2IncludeProfile = oauth2IncludeProfile; + return this; + } + + public AuthRequest oauth2IncludeEmail(String oauth2IncludeProfile) { + this.oauth2IncludeEmail = oauth2IncludeEmail; + return this; + } + + public AuthRequest oauth2Prompt(String oauth2Prompt) { + this.oauth2Prompt = oauth2Prompt; + return this; + } + + public AuthRequest itCaveatTypes(String itCaveatTypes) { + this.itCaveatTypes = itCaveatTypes; + return this; + } + + public AuthRequest putDynamicFiledMap(Map dynamicFields) { + this.dynamicFields = dynamicFields; + return this; + } + + public AuthResponse getResponse() throws IOException { + return HttpFormClient.request(SERVICE_URL, this, AuthResponse.class); + } + + public void getResponseAsync(HttpFormClient.Callback callback) { + HttpFormClient.requestAsync(SERVICE_URL, this, AuthResponse.class, callback); + } +} diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java new file mode 100644 index 0000000000..d00774700d --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013-2017 microG Project Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO: deduplicate file +package org.microg.gms.auth.workaccount; + +import static org.microg.gms.common.HttpFormClient.ResponseField; + +import android.util.Log; + +import java.lang.reflect.Field; + +public class AuthResponse { + private static final String TAG = "GmsAuthResponse"; + + @ResponseField("SID") + public String Sid; + @ResponseField("LSID") + public String LSid; + @ResponseField("Auth") + public String auth; + @ResponseField("Token") + public String token; + @ResponseField("Email") + public String email; + @ResponseField("services") + public String services; + @ResponseField("GooglePlusUpgrade") + public boolean isGooglePlusUpgrade; + @ResponseField("PicasaUser") + public String picasaUserName; + @ResponseField("RopText") + public String ropText; + @ResponseField("RopRevision") + public int ropRevision; + @ResponseField("firstName") + public String firstName; + @ResponseField("lastName") + public String lastName; + @ResponseField("issueAdvice") + public String issueAdvice; + @ResponseField("accountId") + public String accountId; + @ResponseField("Expiry") + public long expiry = -1; + @ResponseField("storeConsentRemotely") + public boolean storeConsentRemotely = true; + @ResponseField("Permission") + public String permission; + @ResponseField("ScopeConsentDetails") + public String scopeConsentDetails; + @ResponseField("ConsentDataBase64") + public String consentDataBase64; + @ResponseField("grantedScopes") + public String grantedScopes; + @ResponseField("itMetadata") + public String itMetadata; + @ResponseField("ResolutionDataBase64") + public String resolutionDataBase64; + @ResponseField("it") + public String auths; + @ResponseField("capabilities") + public String capabilities; + + public static AuthResponse parse(String result) { + AuthResponse response = new AuthResponse(); + String[] entries = result.split("\n"); + for (String s : entries) { + String[] keyValuePair = s.split("=", 2); + String key = keyValuePair[0].trim(); + String value = keyValuePair[1].trim(); + try { + for (Field field : AuthResponse.class.getDeclaredFields()) { + if (field.isAnnotationPresent(ResponseField.class) && + key.equals(field.getAnnotation(ResponseField.class).value())) { + if (field.getType().equals(String.class)) { + field.set(response, value); + } else if (field.getType().equals(boolean.class)) { + field.setBoolean(response, value.equals("1")); + } else if (field.getType().equals(long.class)) { + field.setLong(response, Long.parseLong(value)); + } else if (field.getType().equals(int.class)) { + field.setInt(response, Integer.parseInt(value)); + } + } + } + } catch (Exception e) { + Log.w(TAG, e); + } + } + return response; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("AuthResponse{"); + sb.append("auth='").append(auth).append('\''); + if (Sid != null) sb.append(", Sid='").append(Sid).append('\''); + if (LSid != null) sb.append(", LSid='").append(LSid).append('\''); + if (token != null) sb.append(", token='").append(token).append('\''); + if (email != null) sb.append(", email='").append(email).append('\''); + if (services != null) sb.append(", services='").append(services).append('\''); + if (isGooglePlusUpgrade) sb.append(", isGooglePlusUpgrade=").append(isGooglePlusUpgrade); + if (picasaUserName != null) sb.append(", picasaUserName='").append(picasaUserName).append('\''); + if (ropText != null) sb.append(", ropText='").append(ropText).append('\''); + if (ropRevision != 0) sb.append(", ropRevision=").append(ropRevision); + if (firstName != null) sb.append(", firstName='").append(firstName).append('\''); + if (lastName != null) sb.append(", lastName='").append(lastName).append('\''); + if (issueAdvice != null) sb.append(", issueAdvice='").append(issueAdvice).append('\''); + if (accountId != null) sb.append(", accountId='").append(accountId).append('\''); + if (expiry != -1) sb.append(", expiry=").append(expiry); + if (!storeConsentRemotely) sb.append(", storeConsentRemotely=").append(storeConsentRemotely); + if (permission != null) sb.append(", permission='").append(permission).append('\''); + if (scopeConsentDetails != null) sb.append(", scopeConsentDetails='").append(scopeConsentDetails).append('\''); + if (consentDataBase64 != null) sb.append(", consentDataBase64='").append(consentDataBase64).append('\''); + if (auths != null) sb.append(", auths='").append(auths).append('\''); + if (itMetadata != null) sb.append(", itMetadata='").append(itMetadata).append('\''); + if (resolutionDataBase64 != null) sb.append(", resolutionDataBase64='").append(resolutionDataBase64).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 2de14e1a56..50afeabb63 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -83,9 +83,6 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { token: String? ) { Log.d(TAG, "addWorkAccount with token $token") - if (accountManager.getAccountsByType(WORK_ACCOUNT_TYPE).isNotEmpty()) { - Log.w(TAG, "A work account already exists. Refusing to add a second one.") - } val future = accountManager.addAccount( WORK_ACCOUNT_TYPE, null, @@ -97,7 +94,9 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { ) Thread { future.result.let { result -> - callback?.onAccountAdded( + if (result.containsKey(AccountManager.KEY_ERROR_CODE)) { + Log.w(TAG, "could not add work account due to network error: ${result.getString(AccountManager.KEY_ERROR_MESSAGE)}") + } else callback?.onAccountAdded( Account( result.getString(AccountManager.KEY_ACCOUNT_NAME), result.getString(AccountManager.KEY_ACCOUNT_TYPE) diff --git a/play-services-auth-workaccount/core/src/main/res/values/string.xml b/play-services-auth-workaccount/core/src/main/res/values/string.xml index 7ccab49ede..2073fda931 100644 --- a/play-services-auth-workaccount/core/src/main/res/values/string.xml +++ b/play-services-auth-workaccount/core/src/main/res/values/string.xml @@ -1,7 +1,7 @@ - Mock account for Device Policy Controller - This type of account is not needed for device functionality and cannot be created manually. It may be created automatically by Device Policy Controller apps if needed. + Managed Google for Work account + This type of account is created automatically by your profile administrator if needed. diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java b/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java index fdef586ecc..b6c7e4d2c2 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/HttpFormClient.java @@ -205,14 +205,11 @@ private static T parseResponse(Class tClass, HttpURLConnection connection public static void requestAsync(final String url, final Request request, final Class tClass, final Callback callback) { - new Thread(new Runnable() { - @Override - public void run() { - try { - callback.onResponse(request(url, request, tClass)); - } catch (Exception e) { - callback.onException(e); - } + new Thread(() -> { + try { + callback.onResponse(request(url, request, tClass)); + } catch (Exception e) { + callback.onException(e); } }).start(); } From 3128c063dd3b46d5211ed3d3c4d618e3ef06e35d Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 2 Sep 2024 17:35:33 +0200 Subject: [PATCH 23/59] Add license identifiers and remove unwanted changes --- play-services-auth-workaccount/build.gradle | 6 +----- .../core/src/main/AndroidManifest.xml | 1 - .../auth/account/authenticator/WorkAccountAuthenticator.kt | 5 +++++ .../authenticator/WorkAccountAuthenticatorService.kt | 5 +++++ .../org/microg/gms/auth/workaccount/WorkAccountService.kt | 5 +++++ .../core/src/main/res/drawable/ic_briefcase.xml | 4 ++++ .../core/src/main/res/values/string.xml | 4 ++++ .../core/src/main/res/xml/auth_work_authenticator.xml | 4 ++++ .../android/gms/auth/account/IWorkAccountCallback.aidl | 5 +++++ .../android/gms/auth/account/IWorkAccountService.aidl | 5 +++++ play-services-core/src/main/AndroidManifest.xml | 3 +-- .../com/google/android/gms/auth/account/WorkAccountApi.java | 4 ---- 12 files changed, 39 insertions(+), 12 deletions(-) delete mode 100644 play-services-core/src/main/java/com/google/android/gms/auth/account/WorkAccountApi.java diff --git a/play-services-auth-workaccount/build.gradle b/play-services-auth-workaccount/build.gradle index 0b0dfff78d..22d408c8e5 100644 --- a/play-services-auth-workaccount/build.gradle +++ b/play-services-auth-workaccount/build.gradle @@ -32,11 +32,7 @@ android { apply from: '../gradle/publish-android.gradle' -description = 'microG implementation of play-services-auth-api-phone' +description = 'microG implementation of managed work account support' dependencies { - // Dependencies from play-services-auth-api-phone:18.0.1 - api project(':play-services-base') - api project(':play-services-basement') - api project(':play-services-tasks') } diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml index cea47de20e..ceff9cd5a1 100644 --- a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -15,7 +15,6 @@ - diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 0659669788..c3dfb9e1bc 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 e foundation + * SPDX-License-Identifier: Apache-2.0 + */ + package com.google.android.gms.auth.account.authenticator import android.accounts.AbstractAccountAuthenticator diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt index 1795fa33d0..9f4a7d135e 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticatorService.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 e foundation + * SPDX-License-Identifier: Apache-2.0 + */ + package com.google.android.gms.auth.account.authenticator import android.accounts.AccountManager diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 50afeabb63..d5dc430b7b 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2024 e foundation + * SPDX-License-Identifier: Apache-2.0 + */ + package org.microg.gms.auth.workaccount import android.accounts.Account diff --git a/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml b/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml index 2f9846c48b..8315631566 100644 --- a/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml +++ b/play-services-auth-workaccount/core/src/main/res/drawable/ic_briefcase.xml @@ -1,3 +1,7 @@ + diff --git a/play-services-auth-workaccount/core/src/main/res/values/string.xml b/play-services-auth-workaccount/core/src/main/res/values/string.xml index 2073fda931..de76ed42db 100644 --- a/play-services-auth-workaccount/core/src/main/res/values/string.xml +++ b/play-services-auth-workaccount/core/src/main/res/values/string.xml @@ -1,4 +1,8 @@ + Managed Google for Work account diff --git a/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml b/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml index e0a1155ff7..5e4e22db62 100644 --- a/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml +++ b/play-services-auth-workaccount/core/src/main/res/xml/auth_work_authenticator.xml @@ -1,4 +1,8 @@ + Date: Mon, 2 Sep 2024 18:17:24 +0200 Subject: [PATCH 24/59] Work account: Fix `NewApi` lint failure --- .../microg/gms/auth/workaccount/WorkAccountService.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index d5dc430b7b..3f6145dce3 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -38,7 +38,7 @@ class WorkAccountService : BaseService(TAG, GmsService.WORK_ACCOUNT) { ) { val packageName = PackageUtils.getAndCheckCallingPackage(this, request.packageName) val policyManager = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val authorized = policyManager.isDeviceOwnerApp(packageName) || policyManager.isProfileOwnerApp(packageName) + val authorized = policyManager.isDeviceAdminApp(packageName) if (authorized) { callback.onPostInitCompleteWithConnectionInfo( @@ -59,6 +59,15 @@ class WorkAccountService : BaseService(TAG, GmsService.WORK_ACCOUNT) { } } +private fun DevicePolicyManager.isDeviceAdminApp(packageName: String?): Boolean { + if (packageName == null) return false + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + isDeviceOwnerApp(packageName) || isProfileOwnerApp(packageName) + } else { + isDeviceOwnerApp(packageName) + } +} + class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { val packageManager: PackageManager = context.packageManager From 482038a5b76fc25a47d1f1ded9f9c5a2ea5babb5 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 14 Sep 2024 00:34:14 +0200 Subject: [PATCH 25/59] Deduplicate work account service declaration --- .../core/src/main/AndroidManifest.xml | 10 +++++++--- play-services-core/src/main/AndroidManifest.xml | 8 -------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml index ceff9cd5a1..49a9a15453 100644 --- a/play-services-auth-workaccount/core/src/main/AndroidManifest.xml +++ b/play-services-auth-workaccount/core/src/main/AndroidManifest.xml @@ -18,9 +18,13 @@ - + + + + + + - - - - - - - - From 5a39c65e1e471f71089d8677a77d852011fe6412 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 14 Sep 2024 18:05:15 +0200 Subject: [PATCH 26/59] POC: Test activity in companion app to show client policy --- .../authenticator/WorkAccountAuthenticator.kt | 32 ++++++- vending-app/src/main/AndroidManifest.xml | 9 +- .../org/microg/vending/PlayDebugActivity.kt | 96 +++++++++++++++++++ .../vending/billing/core/GooglePlayApi.kt | 1 + .../vending/billing/core/HeaderProvider.kt | 3 + .../main/proto/EnterpriseClientPolicy.proto | 27 ++++++ vending-app/src/main/proto/SplitInstall.proto | 3 + 7 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt create mode 100644 vending-app/src/main/proto/EnterpriseClientPolicy.proto diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index c3dfb9e1bc..555e6bf885 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -15,6 +15,8 @@ import android.os.Bundle import android.util.Log import com.google.android.gms.auth.workaccount.R import org.microg.gms.auth.workaccount.AuthRequest +import org.microg.gms.auth.workaccount.AuthResponse +import org.microg.gms.common.PackageUtils class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { @@ -114,11 +116,37 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica override fun getAuthToken( response: AccountAuthenticatorResponse?, - account: Account?, + account: Account, authTokenType: String?, options: Bundle? ): Bundle { - TODO("Not yet implemented: getAuthToken") + val authResponse: AuthResponse = + AuthRequest().fromContext(context) + .source("android") + .app(context.packageName, PackageUtils.firstSignatureDigest(context, context.packageName)) + .email(account.name) + .token(AccountManager.get(context).getPassword(account)) + .service(authTokenType) + .delegation(0, null) +// .oauth2Foreground(oauth2Foreground) +// .oauth2Prompt(oauth2Prompt) +// .oauth2IncludeProfile(includeProfile) +// .oauth2IncludeEmail(includeEmail) +// .itCaveatTypes(itCaveatTypes) +// .tokenRequestOptions(tokenRequestOptions) + .systemPartition(true) + .hasPermission(true) +// .putDynamicFiledMap(dynamicFields) + .appIsGms() + .callerIsApp() + .response + + return Bundle().apply { + putString(AccountManager.KEY_ACCOUNT_NAME, account.name) + putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + putString(AccountManager.KEY_AUTHTOKEN, authResponse.auth) + } + } override fun getAuthTokenLabel(authTokenType: String?): String { diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 4530559240..46deb3e155 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -94,7 +94,6 @@ - @@ -186,6 +185,14 @@ + + + + + + + diff --git a/vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt b/vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt new file mode 100644 index 0000000000..e6b8589869 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt @@ -0,0 +1,96 @@ +package org.microg.vending + +import android.accounts.AccountManager +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.vending.buildRequestHeaders +import com.google.android.finsky.AppInstallPolicy +import com.google.android.finsky.GoogleApiResponse +import kotlinx.coroutines.runBlocking +import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.AuthManager +import org.microg.vending.billing.TAG +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY +import org.microg.vending.billing.core.HttpClient +import org.microg.vending.billing.createDeviceEnvInfo + + +class PlayDebugActivity : ComponentActivity() { + + var apps: MutableList> = mutableStateListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + ProfileManager.ensureInitialized(this) + + val am = AccountManager.get(this) + val account = am.getAccountsByType("com.google.work").first()!! + Thread { + runBlocking { + val authData = AuthManager.getAuthData(this@PlayDebugActivity, account) + val deviceInfo = createDeviceEnvInfo(this@PlayDebugActivity) + if (deviceInfo == null || authData == null) { + Log.e( + TAG, + "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData" + ) + return@runBlocking + } + + val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) + .plus("content-type" to "application/x-protobuf") + val client = HttpClient(this@PlayDebugActivity) + + val apps = client.post( + url = URL_ENTERPRISE_CLIENT_POLICY, + headers = headers, + adapter = GoogleApiResponse.ADAPTER + ).response?.enterpriseClientPolicyResult?.policy?.apps?.map { it.packageName!! to it.policy!! } + this@PlayDebugActivity.apps.apply { + clear() + apps?.let { addAll(it) } + } + } + }.start() + + @Composable + fun AppRow(name: String) { + Row(Modifier.padding(16.dp)) { + Text(name) + } + } + + setContent { + MaterialTheme { + Column { + Text(account.name) + LazyColumn(Modifier.padding(16.dp)) { + item { Text("Required apps", style = MaterialTheme.typography.headlineSmall) } + items(apps.filter { it.second == AppInstallPolicy.MANDATORY }.map { it.first }) { + AppRow(it) + } + item { Text("Available apps", style = MaterialTheme.typography.headlineSmall) } + items(apps.filter { it.second == AppInstallPolicy.OPTIONAL }.map { it.first }) { + AppRow(it) + } + } + } + } + } + + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt index 9cab4f4f7e..410640bcdb 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -12,5 +12,6 @@ class GooglePlayApi { const val URL_AUTH_PROOF_TOKENS = "https://www.googleapis.com/reauth/v1beta/users/me/reauthProofTokens" const val URL_DETAILS = "$URL_FDFE/details" const val URL_PURCHASE = "$URL_FDFE/purchase" + const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy" } } \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt index b8ce33284a..bc982f74c8 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/HeaderProvider.kt @@ -1,5 +1,8 @@ package org.microg.vending.billing.core +import android.util.Log +import org.microg.vending.billing.TAG + object HeaderProvider { fun getBaseHeaders(authData: AuthData, deviceInfo: DeviceEnvInfo): MutableMap { val headers: MutableMap = HashMap() diff --git a/vending-app/src/main/proto/EnterpriseClientPolicy.proto b/vending-app/src/main/proto/EnterpriseClientPolicy.proto new file mode 100644 index 0000000000..7d0095b8ea --- /dev/null +++ b/vending-app/src/main/proto/EnterpriseClientPolicy.proto @@ -0,0 +1,27 @@ + +syntax = "proto3"; + +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +message EnterpriseClientPolicyResponse { + optional EnterprisePolicy policy = 1; +} + +message EnterprisePolicy { + repeated App apps = 1; + // There are six more elements with unknown purpose. +} + +message App { + optional string packageName = 1; + optional AppInstallPolicy policy = 2; + optional string emptyString = 4; // = "" + optional int32 unknownNumber = 9; // = 1 +} + +enum AppInstallPolicy { + UNKNOWN = 0; + OPTIONAL = 1; + MANDATORY = 3; +} \ 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 38c7a50043..cb2f5ea491 100644 --- a/vending-app/src/main/proto/SplitInstall.proto +++ b/vending-app/src/main/proto/SplitInstall.proto @@ -2,6 +2,8 @@ option java_package = "com.google.android.finsky"; option java_multiple_files = true; +import "EnterpriseClientPolicy.proto"; + message GoogleApiResponse { optional ApiResponse response = 1; optional UnknownType type= 5; @@ -15,6 +17,7 @@ message UnknownType { message ApiResponse { optional TocResponse tocApi = 6; optional SplitResponse splitReqResult = 21; + optional EnterpriseClientPolicyResponse enterpriseClientPolicyResult = 135; // optional SyncApiResp syncResult = 183; } From 0db6eede9b0d91a3fc9d1337ad504e9e2d875c28 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 15 Sep 2024 14:28:06 +0200 Subject: [PATCH 27/59] Improve visuals of vending activity --- .../authenticator/WorkAccountAuthenticator.kt | 46 ++++--- vending-app/build.gradle | 3 + vending-app/src/main/AndroidManifest.xml | 5 +- .../org/microg/vending/PlayDebugActivity.kt | 96 -------------- .../java/org/microg/vending/enterprise/App.kt | 24 ++++ .../vending/enterprise/EnterpriseApp.kt | 11 ++ .../vending/ui/EnterpriseListComponent.kt | 119 ++++++++++++++++++ .../org/microg/vending/ui/NetworkState.kt | 7 ++ .../vending/ui/NetworkStateComponent.kt | 67 ++++++++++ .../org/microg/vending/ui/VendingActivity.kt | 94 ++++++++++++++ vending-app/src/main/res/values/strings.xml | 10 ++ 11 files changed, 366 insertions(+), 116 deletions(-) delete mode 100644 vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt create mode 100644 vending-app/src/main/java/org/microg/vending/enterprise/App.kt create mode 100644 vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt create mode 100644 vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt create mode 100644 vending-app/src/main/java/org/microg/vending/ui/NetworkState.kt create mode 100644 vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt create mode 100644 vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 555e6bf885..73cce5bc21 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -17,6 +17,7 @@ import com.google.android.gms.auth.workaccount.R import org.microg.gms.auth.workaccount.AuthRequest import org.microg.gms.auth.workaccount.AuthResponse import org.microg.gms.common.PackageUtils +import java.io.IOException class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { @@ -120,33 +121,42 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica authTokenType: String?, options: Bundle? ): Bundle { - val authResponse: AuthResponse = - AuthRequest().fromContext(context) - .source("android") - .app(context.packageName, PackageUtils.firstSignatureDigest(context, context.packageName)) - .email(account.name) - .token(AccountManager.get(context).getPassword(account)) - .service(authTokenType) - .delegation(0, null) + try { + val authResponse: AuthResponse = + AuthRequest().fromContext(context) + .source("android") + .app( + context.packageName, + PackageUtils.firstSignatureDigest(context, context.packageName) + ) + .email(account.name) + .token(AccountManager.get(context).getPassword(account)) + .service(authTokenType) + .delegation(0, null) // .oauth2Foreground(oauth2Foreground) // .oauth2Prompt(oauth2Prompt) // .oauth2IncludeProfile(includeProfile) // .oauth2IncludeEmail(includeEmail) // .itCaveatTypes(itCaveatTypes) // .tokenRequestOptions(tokenRequestOptions) - .systemPartition(true) - .hasPermission(true) + .systemPartition(true) + .hasPermission(true) // .putDynamicFiledMap(dynamicFields) - .appIsGms() - .callerIsApp() - .response + .appIsGms() + .callerIsApp() + .response - return Bundle().apply { - putString(AccountManager.KEY_ACCOUNT_NAME, account.name) - putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) - putString(AccountManager.KEY_AUTHTOKEN, authResponse.auth) + return Bundle().apply { + putString(AccountManager.KEY_ACCOUNT_NAME, account.name) + putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) + putString(AccountManager.KEY_AUTHTOKEN, authResponse.auth) + } + } catch (e: IOException) { + return Bundle().apply { + putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_NETWORK_ERROR) + putString(AccountManager.KEY_ERROR_MESSAGE, e.message) + } } - } override fun getAuthTokenLabel(authTokenType: String?): String { diff --git a/vending-app/build.gradle b/vending-app/build.gradle index c8281eef76..720ac69ab4 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -110,6 +110,9 @@ dependencies { implementation "com.google.android.material:material:$materialVersion" implementation "com.google.accompanist:accompanist-systemuicontroller:0.28.0" + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + //droidguard implementation project(':play-services-droidguard') diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 46deb3e155..5588ca0f2b 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -49,7 +49,8 @@ androidx.compose.ui.unit,androidx.compose.ui.text,androidx.compose.ui.graphics,androidx.compose.ui.geometry, androidx.activity.compose,androidx.compose.runtime.saveable, androidx.compose.material.ripple,androidx.compose.foundation.layout,androidx.compose.animation.core, - coil.singleton, coil.base, androidx.compose.material3, com.google.accompanist.systemuicontroller, androidx.compose.animation.graphics" /> + coil.singleton, coil.base, androidx.compose.material3, com.google.accompanist.systemuicontroller, androidx.compose.animation.graphics, + androidx.compose.ui.tooling.data, androidx.compose.ui.tooling.preview" /> - diff --git a/vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt b/vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt deleted file mode 100644 index e6b8589869..0000000000 --- a/vending-app/src/main/java/org/microg/vending/PlayDebugActivity.kt +++ /dev/null @@ -1,96 +0,0 @@ -package org.microg.vending - -import android.accounts.AccountManager -import android.os.Bundle -import android.util.Log -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.android.vending.buildRequestHeaders -import com.google.android.finsky.AppInstallPolicy -import com.google.android.finsky.GoogleApiResponse -import kotlinx.coroutines.runBlocking -import org.microg.gms.profile.ProfileManager -import org.microg.vending.billing.AuthManager -import org.microg.vending.billing.TAG -import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY -import org.microg.vending.billing.core.HttpClient -import org.microg.vending.billing.createDeviceEnvInfo - - -class PlayDebugActivity : ComponentActivity() { - - var apps: MutableList> = mutableStateListOf() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - ProfileManager.ensureInitialized(this) - - val am = AccountManager.get(this) - val account = am.getAccountsByType("com.google.work").first()!! - Thread { - runBlocking { - val authData = AuthManager.getAuthData(this@PlayDebugActivity, account) - val deviceInfo = createDeviceEnvInfo(this@PlayDebugActivity) - if (deviceInfo == null || authData == null) { - Log.e( - TAG, - "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData" - ) - return@runBlocking - } - - val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) - .plus("content-type" to "application/x-protobuf") - val client = HttpClient(this@PlayDebugActivity) - - val apps = client.post( - url = URL_ENTERPRISE_CLIENT_POLICY, - headers = headers, - adapter = GoogleApiResponse.ADAPTER - ).response?.enterpriseClientPolicyResult?.policy?.apps?.map { it.packageName!! to it.policy!! } - this@PlayDebugActivity.apps.apply { - clear() - apps?.let { addAll(it) } - } - } - }.start() - - @Composable - fun AppRow(name: String) { - Row(Modifier.padding(16.dp)) { - Text(name) - } - } - - setContent { - MaterialTheme { - Column { - Text(account.name) - LazyColumn(Modifier.padding(16.dp)) { - item { Text("Required apps", style = MaterialTheme.typography.headlineSmall) } - items(apps.filter { it.second == AppInstallPolicy.MANDATORY }.map { it.first }) { - AppRow(it) - } - item { Text("Available apps", style = MaterialTheme.typography.headlineSmall) } - items(apps.filter { it.second == AppInstallPolicy.OPTIONAL }.map { it.first }) { - AppRow(it) - } - } - } - } - } - - } -} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt new file mode 100644 index 0000000000..ffafdd50b3 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt @@ -0,0 +1,24 @@ +package org.microg.vending.enterprise + +open class App( + val packageName: String, + val displayName: String, + val state: State, + val iconUrl: String +) { + enum class State { + /** + * App is available, but not installed on the user's device. + */ + NOT_INSTALLED, + /** + * App is already installed on the device, but an update is available. + */ + UPDATE_AVAILABLE, + + /** + * App is installed on device and up to date. + */ + INSTALLED + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt new file mode 100644 index 0000000000..87bce6ca8d --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt @@ -0,0 +1,11 @@ +package org.microg.vending.enterprise + +import com.google.android.finsky.AppInstallPolicy + +class EnterpriseApp( + packageName: String, + displayName: String, + state: State, + iconUrl: String, + val policy: AppInstallPolicy, +) : App(packageName, displayName, state, iconUrl) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt b/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt new file mode 100644 index 0000000000..02ad20200c --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt @@ -0,0 +1,119 @@ +package org.microg.vending.ui + +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.vending.R +import com.google.android.finsky.AppInstallPolicy +import org.microg.vending.enterprise.App +import org.microg.vending.enterprise.EnterpriseApp + + + +@Composable +fun EnterpriseListComponent(apps: List) { + if (apps.isNotEmpty()) LazyColumn(Modifier.padding(16.dp)) { + + val requiredApps = apps.filter { it.policy == AppInstallPolicy.MANDATORY } + if (requiredApps.isNotEmpty()) { + item { InListHeading(R.string.vending_overview_enterprise_row_mandatory) } + item { InListWarning(R.string.vending_overview_enterprise_row_mandatory_hint) } + items(requiredApps) { AppRow(it) } + } + + val optionalApps = apps.filter { it.policy == AppInstallPolicy.OPTIONAL } + if (optionalApps.isNotEmpty()) { + item { InListHeading(R.string.vending_overview_enterprise_row_offered) } + items(optionalApps) { AppRow(it) } + } + } else Box( + Modifier + .fillMaxSize() + .padding(24.dp)) { + Column(Modifier.align(Alignment.Center)) { + Text( + stringResource(R.string.vending_overview_enterprise_no_apps_available), + textAlign = TextAlign.Center + ) + } + } + +} + +@Composable +fun InListHeading(@StringRes text: Int) { + Text( + stringResource(text), + modifier = Modifier.padding(vertical = 8.dp), + style = MaterialTheme.typography.headlineSmall + ) +} + +@Composable +fun InListWarning(@StringRes text: Int) { + Row( + Modifier + .clip(shape = RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.errorContainer) + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + Modifier + .align(Alignment.CenterVertically) + .padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 8.dp), + MaterialTheme.colorScheme.onErrorContainer + ) + Text( + stringResource(text), + Modifier.align(Alignment.CenterVertically) + .padding(top = 8.dp, bottom = 8.dp, end = 16.dp), + MaterialTheme.colorScheme.onErrorContainer + ) + } + +} + +@Composable +fun AppRow(app: App) { + Row(Modifier.padding(16.dp)) { + Text(app.displayName) + } +} + +@Preview +@Composable +fun EnterpriseListComponentPreview() { + EnterpriseListComponent( + listOf( + EnterpriseApp("com.android.vending", "Market", App.State.INSTALLED, "", AppInstallPolicy.MANDATORY), + EnterpriseApp("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, "", AppInstallPolicy.OPTIONAL) + ) + ) +} + +@Preview +@Composable +fun EnterpriseListComponentEmptyPreview() { + EnterpriseListComponent(emptyList()) +} diff --git a/vending-app/src/main/java/org/microg/vending/ui/NetworkState.kt b/vending-app/src/main/java/org/microg/vending/ui/NetworkState.kt new file mode 100644 index 0000000000..e6397bbc26 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/NetworkState.kt @@ -0,0 +1,7 @@ +package org.microg.vending.ui + +enum class NetworkState { + ACTIVE, + PASSIVE, + ERROR +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt b/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt new file mode 100644 index 0000000000..fff735b40b --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt @@ -0,0 +1,67 @@ +package org.microg.vending.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.vending.R + +@Composable +fun NetworkStateComponent(networkState: NetworkState, retry: () -> Unit, content: @Composable () -> Unit) { + when (networkState) { + NetworkState.ACTIVE -> { + Box(Modifier.fillMaxSize()) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = MaterialTheme.colorScheme.primary + ) + } + } + + NetworkState.ERROR -> { + Box(Modifier.fillMaxSize().padding(24.dp)) { + Column(Modifier.align(Alignment.Center)) { + Text(stringResource(R.string.error_network)) + Button(retry, Modifier.align(Alignment.CenterHorizontally).padding(top = 8.dp)) { + Text(stringResource(R.string.error_retry)) + } + } + } + } + + NetworkState.PASSIVE -> { + content() + } + } +} + + +@Preview +@Composable +fun NetworkStateComponentActivePreview() { + NetworkStateComponent(NetworkState.ACTIVE, { }) {} +} + +@Preview +@Composable +fun NetworkStateComponentErrorPreview() { + NetworkStateComponent(NetworkState.ERROR, { }) {} +} + +@Preview +@Composable +fun NetworkStateComponentPassivePreview() { + NetworkStateComponent(NetworkState.PASSIVE, {}) { + Text("Network operation complete.", Modifier.padding(16.dp)) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt new file mode 100644 index 0000000000..d8d17747b5 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -0,0 +1,94 @@ +package org.microg.vending.ui + +import android.accounts.AccountManager +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.android.vending.buildRequestHeaders +import com.android.volley.VolleyError +import com.google.android.finsky.GoogleApiResponse +import kotlinx.coroutines.runBlocking +import org.microg.gms.profile.ProfileManager +import org.microg.vending.billing.AuthManager +import org.microg.vending.billing.TAG +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY +import org.microg.vending.billing.core.HttpClient +import org.microg.vending.billing.createDeviceEnvInfo +import org.microg.vending.enterprise.App +import org.microg.vending.enterprise.EnterpriseApp + + +class VendingActivity : ComponentActivity() { + + var apps: MutableList = mutableStateListOf() + var networkState by mutableStateOf(NetworkState.ACTIVE) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + ProfileManager.ensureInitialized(this) + + val am = AccountManager.get(this) + val account = am.getAccountsByType("com.google.work").first()!! + Thread { + runBlocking { + val authData = AuthManager.getAuthData(this@VendingActivity, account) + val deviceInfo = createDeviceEnvInfo(this@VendingActivity) + if (deviceInfo == null || authData == null) { + Log.e(TAG, "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData") + return@runBlocking + } + + val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) + .plus("content-type" to "application/x-protobuf") + val client = HttpClient(this@VendingActivity) + + try { + val apps = client.post( + url = URL_ENTERPRISE_CLIENT_POLICY, + headers = headers, + adapter = GoogleApiResponse.ADAPTER + ).response?.enterpriseClientPolicyResult?.policy?.apps?.filter { it.packageName != null && it.policy != null } + ?.map { + EnterpriseApp( + it.packageName!!, + "Display name placeholder", + App.State.NOT_INSTALLED, + "https://i.ytimg.com/vi/IWZFLZ1mvc4/hqdefault.jpg", + it.policy!! + ) + } + this@VendingActivity.apps.apply { + clear() + apps?.let { addAll(it) } + } + networkState = NetworkState.PASSIVE + } catch (e: VolleyError) { + networkState = NetworkState.ERROR + } + } + }.start() + + + + setContent { + MaterialTheme { + Column { + Text(account.name) + NetworkStateComponent(networkState, { TODO("reload") }) { + EnterpriseListComponent(apps) + } + } + } + } + + } +} \ 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 0393927deb..41cb93d525 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -17,12 +17,22 @@ Sign In Ignore + Installation required + Your administrator requires that you install these apps on your managed device. + Your device is missing mandatory apps chosen by your administrator. + Available apps + These are all the apps made available by your enterprise. + No apps have been made available by your administrator + Update available + Installed apps + Pay currently not possible Confirm Purchase Not connected to the internet. Please make sure Wi-Fi or mobile network is turned on and try again. The password you entered is incorrect. Unknown error, please exit and try again. + Retry Enter your password Remember my login on this device Forget password? From c62109805a102bc084a13fa4f36ddf589ccb21ca Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 15 Sep 2024 17:32:37 +0200 Subject: [PATCH 28/59] Fetch bulk details for apps retrieved from policy Nonfunctional attempt. --- vending-app/src/main/AndroidManifest.xml | 4 +- .../vending/billing/core/GooglePlayApi.kt | 1 + .../java/org/microg/vending/enterprise/App.kt | 2 +- .../vending/enterprise/EnterpriseApp.kt | 2 +- .../org/microg/vending/ui/VendingActivity.kt | 117 ++++++++++++++---- .../kotlin/com/android/vending/extensions.kt | 2 +- .../src/main/proto/BulkDetailsRequest.proto | 15 +++ .../src/main/proto/BulkDetailsResponse.proto | 29 +++++ .../src/main/proto/RequestHeader.proto | 2 +- vending-app/src/main/proto/SplitInstall.proto | 2 + vending-app/src/main/res/drawable/ic_work.xml | 5 + vending-app/src/main/res/values/strings.xml | 1 + 12 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 vending-app/src/main/proto/BulkDetailsRequest.proto create mode 100644 vending-app/src/main/proto/BulkDetailsResponse.proto create mode 100644 vending-app/src/main/res/drawable/ic_work.xml diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 5588ca0f2b..023cc56063 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -187,7 +187,9 @@ android:name="com.google.android.finsky.splitinstallservice.SplitInstallManager$InstallResultReceiver" android:exported="true"/> + android:exported="true" + android:theme="@style/Theme.Material3.DayNight" + android:label="@string/vending_activity_name"> diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt index 410640bcdb..025ea9d1a2 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -11,6 +11,7 @@ class GooglePlayApi { const val URL_GET_PURCHASE_HISTORY = "$URL_FDFE/inAppPurchaseHistory" const val URL_AUTH_PROOF_TOKENS = "https://www.googleapis.com/reauth/v1beta/users/me/reauthProofTokens" const val URL_DETAILS = "$URL_FDFE/details" + const val URL_BULK_DETAILS = "$URL_FDFE/bulkDetails" const val URL_PURCHASE = "$URL_FDFE/purchase" const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy" } diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt index ffafdd50b3..744be08abf 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt @@ -4,7 +4,7 @@ open class App( val packageName: String, val displayName: String, val state: State, - val iconUrl: String + val iconUrl: String? ) { enum class State { /** diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt index 87bce6ca8d..d31169e07e 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt @@ -6,6 +6,6 @@ class EnterpriseApp( packageName: String, displayName: String, state: State, - iconUrl: String, + iconUrl: String?, val policy: AppInstallPolicy, ) : App(packageName, displayName, state, iconUrl) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index d8d17747b5..a6f51415a9 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -5,25 +5,46 @@ import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.vending.R import com.android.vending.buildRequestHeaders import com.android.volley.VolleyError +import com.google.android.finsky.BulkDetailsRequest +import com.google.android.finsky.DetailsRequest import com.google.android.finsky.GoogleApiResponse import kotlinx.coroutines.runBlocking import org.microg.gms.profile.ProfileManager +import org.microg.gms.ui.TAG import org.microg.vending.billing.AuthManager -import org.microg.vending.billing.TAG +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_BULK_DETAILS +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo +import org.microg.vending.billing.proto.ResponseWrapper import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp +import java.io.IOException class VendingActivity : ComponentActivity() { @@ -31,7 +52,9 @@ class VendingActivity : ComponentActivity() { var apps: MutableList = mutableStateListOf() var networkState by mutableStateOf(NetworkState.ACTIVE) + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) ProfileManager.ensureInitialized(this) @@ -40,51 +63,97 @@ class VendingActivity : ComponentActivity() { val account = am.getAccountsByType("com.google.work").first()!! Thread { runBlocking { - val authData = AuthManager.getAuthData(this@VendingActivity, account) - val deviceInfo = createDeviceEnvInfo(this@VendingActivity) - if (deviceInfo == null || authData == null) { - Log.e(TAG, "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData") - return@runBlocking - } + try { + val authData = AuthManager.getAuthData(this@VendingActivity, account) + val deviceInfo = createDeviceEnvInfo(this@VendingActivity) + if (deviceInfo == null || authData == null) { + Log.e(TAG, "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData") + networkState = NetworkState.ERROR + return@runBlocking + } - val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) - .plus("content-type" to "application/x-protobuf") - val client = HttpClient(this@VendingActivity) + val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) + val client = HttpClient(this@VendingActivity) - try { val apps = client.post( url = URL_ENTERPRISE_CLIENT_POLICY, - headers = headers, + headers = headers.plus("content-type" to "application/x-protobuf"), adapter = GoogleApiResponse.ADAPTER ).response?.enterpriseClientPolicyResult?.policy?.apps?.filter { it.packageName != null && it.policy != null } - ?.map { + + if (apps == null) { + Log.e(TAG, "unexpected network response: missing expected fields") + networkState = NetworkState.ERROR + return@runBlocking + } + + val details = client.post( + url = URL_BULK_DETAILS, + headers = headers, + adapter = GoogleApiResponse.ADAPTER, + payload = BulkDetailsRequest( + apps.map { + DetailsRequest( + packageName = it.packageName!!, + versionCode = 0, + unknown0 = 0 + ) + } + )).response?.bulkDetailsResponse?.details?.mapNotNull { it.metadata } + ?.filter { it.packageName != null && it.displayName != null }?.map { app -> EnterpriseApp( - it.packageName!!, - "Display name placeholder", + app.packageName!!, + app.displayName!!, App.State.NOT_INSTALLED, - "https://i.ytimg.com/vi/IWZFLZ1mvc4/hqdefault.jpg", - it.policy!! + app.icon.lastOrNull()?.url, + apps.find { it.packageName!! == app.packageName }!!.policy!!, ) } + this@VendingActivity.apps.apply { clear() - apps?.let { addAll(it) } + details?.let { addAll(it) } } networkState = NetworkState.PASSIVE + } catch (e: IOException) { + networkState = NetworkState.ERROR } catch (e: VolleyError) { networkState = NetworkState.ERROR } } }.start() - - setContent { MaterialTheme { - Column { - Text(account.name) - NetworkStateComponent(networkState, { TODO("reload") }) { - EnterpriseListComponent(apps) + Scaffold( + topBar = { + TopAppBar( + title = { + Row { + Icon( + painterResource(R.drawable.ic_work), + contentDescription = null, + Modifier.align(Alignment.CenterVertically), + tint = LocalContentColor.current + ) + Text(stringResource(R.string.vending_activity_name), + Modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp) + ) + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ) + ) + } + ) { innerPadding -> + Column(Modifier.padding(innerPadding)) { + NetworkStateComponent(networkState, { TODO("reload") }) { + EnterpriseListComponent(apps) + } } } } diff --git a/vending-app/src/main/kotlin/com/android/vending/extensions.kt b/vending-app/src/main/kotlin/com/android/vending/extensions.kt index 5b010d2c32..34fee97b25 100644 --- a/vending-app/src/main/kotlin/com/android/vending/extensions.kt +++ b/vending-app/src/main/kotlin/com/android/vending/extensions.kt @@ -60,7 +60,7 @@ fun buildRequestHeaders(auth: String, androidId: Long, language: List ?= 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() + UnknownByte12.Builder().bytes(ByteString.EMPTY).build().toString() ).unknown2(1).build() ).userAgent( UserAgent.Builder().deviceName(Build.DEVICE).deviceHardware(Build.HARDWARE).deviceModelName(Build.MODEL).finskyVersion(FINSKY_VERSION) diff --git a/vending-app/src/main/proto/BulkDetailsRequest.proto b/vending-app/src/main/proto/BulkDetailsRequest.proto new file mode 100644 index 0000000000..6797a70b23 --- /dev/null +++ b/vending-app/src/main/proto/BulkDetailsRequest.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; + +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + + +message BulkDetailsRequest { + repeated DetailsRequest requests = 8; +} + +message DetailsRequest { + required string packageName = 1; + optional uint32 versionCode = 2; + required uint32 unknown0 = 3; // = 0 +} \ No newline at end of file diff --git a/vending-app/src/main/proto/BulkDetailsResponse.proto b/vending-app/src/main/proto/BulkDetailsResponse.proto new file mode 100644 index 0000000000..6450d4b3a2 --- /dev/null +++ b/vending-app/src/main/proto/BulkDetailsResponse.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +option java_package = "com.google.android.finsky"; +option java_multiple_files = true; + +message BulkAppDetailsResponse { + repeated AppDetail details = 1; +} + +message AppDetail { + optional AppMetadata metadata = 1; +} + +message AppMetadata { + optional string packageName = 1; // duplicated at ID 2 + optional string displayName = 5; + optional string author = 6; + repeated Icon icon = 10; +} + +message Icon { + optional Resolution resolution = 2; + string url = 5; +} + +message Resolution { + optional uint32 width = 3; + optional uint32 height = 4; +} \ No newline at end of file diff --git a/vending-app/src/main/proto/RequestHeader.proto b/vending-app/src/main/proto/RequestHeader.proto index 4fb678a558..2dea2e09d4 100644 --- a/vending-app/src/main/proto/RequestHeader.proto +++ b/vending-app/src/main/proto/RequestHeader.proto @@ -43,7 +43,7 @@ message RequestLanguagePackage { message DeviceMeta { optional AndroidVersionMeta android = 1; - optional UnknownByte12 unknown1 = 2; + optional string unknown1 = 2; // inconsistent observations; a field of type "UnknownByte12" was observed as well optional uint32 unknown2 = 3; // observed value: 1 } diff --git a/vending-app/src/main/proto/SplitInstall.proto b/vending-app/src/main/proto/SplitInstall.proto index cb2f5ea491..abb4dcca9f 100644 --- a/vending-app/src/main/proto/SplitInstall.proto +++ b/vending-app/src/main/proto/SplitInstall.proto @@ -3,6 +3,7 @@ option java_package = "com.google.android.finsky"; option java_multiple_files = true; import "EnterpriseClientPolicy.proto"; +import "BulkDetailsResponse.proto"; message GoogleApiResponse { optional ApiResponse response = 1; @@ -16,6 +17,7 @@ message UnknownType { message ApiResponse { optional TocResponse tocApi = 6; + optional BulkAppDetailsResponse bulkDetailsResponse = 19; optional SplitResponse splitReqResult = 21; optional EnterpriseClientPolicyResponse enterpriseClientPolicyResult = 135; // optional SyncApiResp syncResult = 183; diff --git a/vending-app/src/main/res/drawable/ic_work.xml b/vending-app/src/main/res/drawable/ic_work.xml new file mode 100644 index 0000000000..2f9846c48b --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_work.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 41cb93d525..4616d10f39 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Sign In Ignore + Work app store Installation required Your administrator requires that you install these apps on your managed device. Your device is missing mandatory apps chosen by your administrator. From 1e17cc1cf2d01a1ef490f22da5b7cac81c1dae71 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 16 Sep 2024 14:06:27 +0200 Subject: [PATCH 29/59] Load app details using getItems --- vending-app/build.gradle | 6 +- .../vending/billing/core/GooglePlayApi.kt | 1 + .../vending/ui/EnterpriseListComponent.kt | 69 ++++++++++++------- .../vending/ui/NetworkStateComponent.kt | 4 +- .../org/microg/vending/ui/VendingActivity.kt | 32 ++++++++- .../src/main/proto/GetItemsRequest.proto | 24 +++++++ .../src/main/proto/GetItemsResponse.proto | 49 +++++++++++++ vending-app/src/main/res/values/strings.xml | 2 +- 8 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 vending-app/src/main/proto/GetItemsRequest.proto create mode 100644 vending-app/src/main/proto/GetItemsResponse.proto diff --git a/vending-app/build.gradle b/vending-app/build.gradle index 720ac69ab4..ac6bd6f35c 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -93,11 +93,12 @@ dependencies { implementation project(':play-services-base-core') implementation "com.squareup.wire:wire-runtime:$wireVersion" + implementation "com.squareup.wire:wire-grpc-client:$wireVersion" + implementation "com.android.volley:volley:$volleyVersion" implementation "androidx.webkit:webkit:$webkitVersion" - implementation "com.squareup.wire:wire-grpc-client:$wireVersion" //compose implementation platform('androidx.compose:compose-bom:2022.10.00') @@ -113,6 +114,9 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' + // Coil (image loading) + implementation("io.coil-kt:coil-compose:2.7.0") + //droidguard implementation project(':play-services-droidguard') diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt index 025ea9d1a2..68182627da 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -12,6 +12,7 @@ class GooglePlayApi { const val URL_AUTH_PROOF_TOKENS = "https://www.googleapis.com/reauth/v1beta/users/me/reauthProofTokens" const val URL_DETAILS = "$URL_FDFE/details" const val URL_BULK_DETAILS = "$URL_FDFE/bulkDetails" + const val URL_ITEM_DETAILS = "$URL_FDFE/getItems" const val URL_PURCHASE = "$URL_FDFE/purchase" const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy" } diff --git a/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt b/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt index 02ad20200c..32c74e14a9 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt @@ -1,12 +1,17 @@ package org.microg.vending.ui +import android.util.Log +import android.widget.Space import androidx.annotation.StringRes import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -23,8 +28,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.android.vending.R import com.google.android.finsky.AppInstallPolicy +import org.microg.gms.ui.TAG import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp @@ -46,10 +53,12 @@ fun EnterpriseListComponent(apps: List) { item { InListHeading(R.string.vending_overview_enterprise_row_offered) } items(optionalApps) { AppRow(it) } } + } else Box( Modifier .fillMaxSize() - .padding(24.dp)) { + .padding(24.dp) + ) { Column(Modifier.align(Alignment.Center)) { Text( stringResource(R.string.vending_overview_enterprise_no_apps_available), @@ -64,39 +73,52 @@ fun EnterpriseListComponent(apps: List) { fun InListHeading(@StringRes text: Int) { Text( stringResource(text), - modifier = Modifier.padding(vertical = 8.dp), + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), style = MaterialTheme.typography.headlineSmall ) } @Composable fun InListWarning(@StringRes text: Int) { - Row( - Modifier - .clip(shape = RoundedCornerShape(16.dp)) - .background(MaterialTheme.colorScheme.errorContainer) - ) { - Icon( - Icons.Default.Warning, - contentDescription = null, + Column(Modifier.padding(bottom = 8.dp)) { + Row( Modifier - .align(Alignment.CenterVertically) - .padding(start = 16.dp, top = 8.dp, bottom = 8.dp, end = 8.dp), - MaterialTheme.colorScheme.onErrorContainer - ) - Text( - stringResource(text), - Modifier.align(Alignment.CenterVertically) - .padding(top = 8.dp, bottom = 8.dp, end = 16.dp), - MaterialTheme.colorScheme.onErrorContainer - ) + .clip(shape = RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.errorContainer), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 16.dp), + MaterialTheme.colorScheme.onErrorContainer + ) + Text( + stringResource(text), + Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp), + MaterialTheme.colorScheme.onErrorContainer + ) + } } } @Composable fun AppRow(app: App) { - Row(Modifier.padding(16.dp)) { + Row( + Modifier.padding(top = 8.dp, bottom = 8.dp, end = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + if (app.iconUrl != null) { + AsyncImage( + model = app.iconUrl, + modifier = Modifier.size(48.dp), + contentDescription = null, + ) + } else { + Spacer(Modifier.size(48.dp)) + } Text(app.displayName) } } @@ -106,8 +128,9 @@ fun AppRow(app: App) { fun EnterpriseListComponentPreview() { EnterpriseListComponent( listOf( - EnterpriseApp("com.android.vending", "Market", App.State.INSTALLED, "", AppInstallPolicy.MANDATORY), - EnterpriseApp("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, "", AppInstallPolicy.OPTIONAL) + EnterpriseApp("com.android.vending", "Market", App.State.INSTALLED, null, AppInstallPolicy.MANDATORY), + EnterpriseApp("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, null, AppInstallPolicy.OPTIONAL), + EnterpriseApp("org.thoughtcrime.securesms", "Signal", App.State.NOT_INSTALLED, null, AppInstallPolicy.OPTIONAL) ) ) } diff --git a/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt b/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt index fff735b40b..2434fe376d 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt @@ -30,9 +30,9 @@ fun NetworkStateComponent(networkState: NetworkState, retry: () -> Unit, content NetworkState.ERROR -> { Box(Modifier.fillMaxSize().padding(24.dp)) { - Column(Modifier.align(Alignment.Center)) { + Column(Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally) { Text(stringResource(R.string.error_network)) - Button(retry, Modifier.align(Alignment.CenterHorizontally).padding(top = 8.dp)) { + Button(retry, Modifier.padding(top = 8.dp)) { Text(stringResource(R.string.error_retry)) } } diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index a6f51415a9..140ab2522a 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -1,5 +1,10 @@ package org.microg.vending.ui +import AppMeta +import GetItemsRequest +import GetItemsResponse +import RequestApp +import RequestItem import android.accounts.AccountManager import android.os.Bundle import android.util.Log @@ -39,6 +44,7 @@ import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_BULK_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo import org.microg.vending.billing.proto.ResponseWrapper @@ -88,6 +94,26 @@ class VendingActivity : ComponentActivity() { } val details = client.post( + url = URL_ITEM_DETAILS, + // TODO: meaning unclear, but returns 400 without. constant? possible has influence on which fields are returned? + headers = headers.plus("x-dfe-item-field-mask" to "GgJGCCIKBgIAXASAAAAAAQ"), + adapter = GetItemsResponse.ADAPTER, + payload = GetItemsRequest( + apps.map { + RequestItem(RequestApp(AppMeta(it.packageName!!))) + } + ) + ).items.map { it.response }.map { item -> + EnterpriseApp( + item!!.meta!!.packageName!!, + item.detail!!.name!!.displayName!!, + App.State.NOT_INSTALLED, + item.detail?.icon?.icon?.paint?.url, + apps.find { it.packageName!! == item.meta!!.packageName }!!.policy!!, + ) + } + + /*val details = client.post( url = URL_BULK_DETAILS, headers = headers, adapter = GoogleApiResponse.ADAPTER, @@ -108,17 +134,19 @@ class VendingActivity : ComponentActivity() { app.icon.lastOrNull()?.url, apps.find { it.packageName!! == app.packageName }!!.policy!!, ) - } + }*/ this@VendingActivity.apps.apply { clear() - details?.let { addAll(it) } + addAll(details) } networkState = NetworkState.PASSIVE } catch (e: IOException) { networkState = NetworkState.ERROR } catch (e: VolleyError) { networkState = NetworkState.ERROR + } catch (e: NullPointerException) { + networkState = NetworkState.ERROR } } }.start() diff --git a/vending-app/src/main/proto/GetItemsRequest.proto b/vending-app/src/main/proto/GetItemsRequest.proto new file mode 100644 index 0000000000..ae76ed7f8e --- /dev/null +++ b/vending-app/src/main/proto/GetItemsRequest.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +message GetItemsRequest { + repeated RequestItem items = 2; +} + +// Reason for hierarchy is unknown. + +message RequestItem { + RequestApp app = 1; + LocalData local = 2; +} + +message RequestApp { + AppMeta meta = 1; +} + +message AppMeta { + optional string packageName = 1; +} + +message LocalData { + // suspected to contain local version code and signature, if available +} \ No newline at end of file diff --git a/vending-app/src/main/proto/GetItemsResponse.proto b/vending-app/src/main/proto/GetItemsResponse.proto new file mode 100644 index 0000000000..ad2ab7a007 --- /dev/null +++ b/vending-app/src/main/proto/GetItemsResponse.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; + +import "GetItemsRequest.proto"; + +message GetItemsResponse { + // technically this is a typical Google response containing the typical `GoogleApiResponse` fields, + // but field 1.145 only encodes some kind of index and the real content is at field 11 + repeated ItemQuery items = 11; +} + +message ItemQuery { + optional RequestApp query = 1; + optional ItemResponse response = 2; +} + +message ItemResponse { + optional AppMeta meta = 1; + optional ItemAppDetail detail = 2; + optional ItemOffer offer = 3; +} + +message ItemAppDetail { + optional Name name = 1; + optional ItemIcon icon = 2; +} + +message Name { + optional string displayName = 1; +} + +message ItemIcon { + optional IconVariant icon = 1; +} + +message IconVariant { + optional IconPaint paint = 6; +} + +message IconPaint { + optional string url = 1; +} + +message ItemOffer { + optional ItemDelivery delivery = 28; +} + +message ItemDelivery { + optional string key = 14; +} diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index 4616d10f39..b865e8b867 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -19,7 +19,7 @@ Work app store Installation required - Your administrator requires that you install these apps on your managed device. + Your administrator requires these apps to be installed on your managed device. Your device is missing mandatory apps chosen by your administrator. Available apps These are all the apps made available by your enterprise. From 8e5ca13f0b4e0d42754e6b399fd83bf4fd10e967 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 16 Sep 2024 14:37:48 +0200 Subject: [PATCH 30/59] Cleanup: Remove nonfunctional bulkDetails request --- .../vending/billing/core/GooglePlayApi.kt | 1 - .../org/microg/vending/ui/VendingActivity.kt | 33 +++---------------- .../src/main/proto/BulkDetailsRequest.proto | 15 --------- .../src/main/proto/BulkDetailsResponse.proto | 29 ---------------- vending-app/src/main/proto/SplitInstall.proto | 2 -- 5 files changed, 4 insertions(+), 76 deletions(-) delete mode 100644 vending-app/src/main/proto/BulkDetailsRequest.proto delete mode 100644 vending-app/src/main/proto/BulkDetailsResponse.proto diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt index 68182627da..54e92507da 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -11,7 +11,6 @@ class GooglePlayApi { const val URL_GET_PURCHASE_HISTORY = "$URL_FDFE/inAppPurchaseHistory" const val URL_AUTH_PROOF_TOKENS = "https://www.googleapis.com/reauth/v1beta/users/me/reauthProofTokens" const val URL_DETAILS = "$URL_FDFE/details" - const val URL_BULK_DETAILS = "$URL_FDFE/bulkDetails" const val URL_ITEM_DETAILS = "$URL_FDFE/getItems" const val URL_PURCHASE = "$URL_FDFE/purchase" const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy" diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 140ab2522a..690a46643c 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -34,15 +34,11 @@ import androidx.compose.ui.unit.dp import com.android.vending.R import com.android.vending.buildRequestHeaders import com.android.volley.VolleyError -import com.google.android.finsky.BulkDetailsRequest -import com.google.android.finsky.DetailsRequest import com.google.android.finsky.GoogleApiResponse import kotlinx.coroutines.runBlocking import org.microg.gms.profile.ProfileManager import org.microg.gms.ui.TAG import org.microg.vending.billing.AuthManager -import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_BULK_DETAILS -import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient @@ -93,9 +89,11 @@ class VendingActivity : ComponentActivity() { return@runBlocking } + Log.v(TAG, "app policy: ${apps.joinToString { "${it.packageName}: ${it.policy}" }}") + val details = client.post( url = URL_ITEM_DETAILS, - // TODO: meaning unclear, but returns 400 without. constant? possible has influence on which fields are returned? + // TODO: meaning unclear, but returns 400 without. constant? possibly has influence on which fields are returned? headers = headers.plus("x-dfe-item-field-mask" to "GgJGCCIKBgIAXASAAAAAAQ"), adapter = GetItemsResponse.ADAPTER, payload = GetItemsRequest( @@ -108,34 +106,11 @@ class VendingActivity : ComponentActivity() { item!!.meta!!.packageName!!, item.detail!!.name!!.displayName!!, App.State.NOT_INSTALLED, - item.detail?.icon?.icon?.paint?.url, + item.detail.icon?.icon?.paint?.url, apps.find { it.packageName!! == item.meta!!.packageName }!!.policy!!, ) } - /*val details = client.post( - url = URL_BULK_DETAILS, - headers = headers, - adapter = GoogleApiResponse.ADAPTER, - payload = BulkDetailsRequest( - apps.map { - DetailsRequest( - packageName = it.packageName!!, - versionCode = 0, - unknown0 = 0 - ) - } - )).response?.bulkDetailsResponse?.details?.mapNotNull { it.metadata } - ?.filter { it.packageName != null && it.displayName != null }?.map { app -> - EnterpriseApp( - app.packageName!!, - app.displayName!!, - App.State.NOT_INSTALLED, - app.icon.lastOrNull()?.url, - apps.find { it.packageName!! == app.packageName }!!.policy!!, - ) - }*/ - this@VendingActivity.apps.apply { clear() addAll(details) diff --git a/vending-app/src/main/proto/BulkDetailsRequest.proto b/vending-app/src/main/proto/BulkDetailsRequest.proto deleted file mode 100644 index 6797a70b23..0000000000 --- a/vending-app/src/main/proto/BulkDetailsRequest.proto +++ /dev/null @@ -1,15 +0,0 @@ -syntax = "proto2"; - -option java_package = "com.google.android.finsky"; -option java_multiple_files = true; - - -message BulkDetailsRequest { - repeated DetailsRequest requests = 8; -} - -message DetailsRequest { - required string packageName = 1; - optional uint32 versionCode = 2; - required uint32 unknown0 = 3; // = 0 -} \ No newline at end of file diff --git a/vending-app/src/main/proto/BulkDetailsResponse.proto b/vending-app/src/main/proto/BulkDetailsResponse.proto deleted file mode 100644 index 6450d4b3a2..0000000000 --- a/vending-app/src/main/proto/BulkDetailsResponse.proto +++ /dev/null @@ -1,29 +0,0 @@ -syntax = "proto3"; - -option java_package = "com.google.android.finsky"; -option java_multiple_files = true; - -message BulkAppDetailsResponse { - repeated AppDetail details = 1; -} - -message AppDetail { - optional AppMetadata metadata = 1; -} - -message AppMetadata { - optional string packageName = 1; // duplicated at ID 2 - optional string displayName = 5; - optional string author = 6; - repeated Icon icon = 10; -} - -message Icon { - optional Resolution resolution = 2; - string url = 5; -} - -message Resolution { - optional uint32 width = 3; - optional uint32 height = 4; -} \ 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 abb4dcca9f..cb2f5ea491 100644 --- a/vending-app/src/main/proto/SplitInstall.proto +++ b/vending-app/src/main/proto/SplitInstall.proto @@ -3,7 +3,6 @@ option java_package = "com.google.android.finsky"; option java_multiple_files = true; import "EnterpriseClientPolicy.proto"; -import "BulkDetailsResponse.proto"; message GoogleApiResponse { optional ApiResponse response = 1; @@ -17,7 +16,6 @@ message UnknownType { message ApiResponse { optional TocResponse tocApi = 6; - optional BulkAppDetailsResponse bulkDetailsResponse = 19; optional SplitResponse splitReqResult = 21; optional EnterpriseClientPolicyResponse enterpriseClientPolicyResult = 135; // optional SyncApiResp syncResult = 183; From 20e04fe0fcfa829adced4107a5dbd7bf684c34c6 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 16 Sep 2024 15:53:19 +0200 Subject: [PATCH 31/59] UI: Add download button --- .../org/microg/vending/ui/VendingActivity.kt | 7 +- .../microg/vending/ui/components/AppRow.kt | 86 +++++++++++++++++++ .../EnterpriseList.kt} | 44 ++-------- .../NetworkState.kt} | 17 ++-- .../src/main/res/drawable/ic_download.xml | 9 ++ .../src/main/res/drawable/ic_update.xml | 16 ++++ vending-app/src/main/res/drawable/ic_work.xml | 4 + vending-app/src/main/res/values/strings.xml | 3 + 8 files changed, 139 insertions(+), 47 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt rename vending-app/src/main/java/org/microg/vending/ui/{EnterpriseListComponent.kt => components/EnterpriseList.kt} (75%) rename vending-app/src/main/java/org/microg/vending/ui/{NetworkStateComponent.kt => components/NetworkState.kt} (79%) create mode 100644 vending-app/src/main/res/drawable/ic_download.xml create mode 100644 vending-app/src/main/res/drawable/ic_update.xml diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 690a46643c..d52483dec8 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -43,9 +43,10 @@ import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CL import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo -import org.microg.vending.billing.proto.ResponseWrapper import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp +import org.microg.vending.ui.components.EnterpriseList +import org.microg.vending.ui.components.NetworkState import java.io.IOException @@ -154,8 +155,8 @@ class VendingActivity : ComponentActivity() { } ) { innerPadding -> Column(Modifier.padding(innerPadding)) { - NetworkStateComponent(networkState, { TODO("reload") }) { - EnterpriseListComponent(apps) + NetworkState(networkState, { TODO("reload") }) { + EnterpriseList(apps) } } } diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt new file mode 100644 index 0000000000..40d986a8db --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt @@ -0,0 +1,86 @@ +package org.microg.vending.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.android.vending.R +import org.microg.vending.enterprise.App + +@Composable +fun AppRow(app: App) { + Row( + Modifier.padding(top = 8.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically + ) { + val iconSpace = Modifier.size(48.dp) + if (app.iconUrl != null) { + AsyncImage( + model = app.iconUrl, + modifier = iconSpace, + contentDescription = null, + ) + } else { + Spacer(iconSpace) + } + Text(app.displayName) + Spacer(Modifier.weight(1f)) + if (app.state != App.State.NOT_INSTALLED) { + IconButton({}) { + Icon(Icons.Default.Delete, stringResource(R.string.vending_overview_row_action_uninstall), tint = MaterialTheme.colorScheme.secondary) + } + } + if (app.state == App.State.UPDATE_AVAILABLE) { + FilledIconButton({}, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + Icon(painterResource(R.drawable.ic_update), stringResource(R.string.vending_overview_row_action_update), tint = MaterialTheme.colorScheme.secondary) + } + } + if (app.state == App.State.NOT_INSTALLED) + FilledIconButton({}, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary) + } + } +} + +@Preview +@Composable +fun AppRowNotInstalledPreview() { + AppRow(App("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, null)) +} + +@Preview +@Composable +fun AppRowUpdateablePreview() { + AppRow(App("org.mozilla.firefox", "Firefox", App.State.UPDATE_AVAILABLE, null)) +} + +@Preview +@Composable +fun AppRowInstalledPreview() { + AppRow(App("org.mozilla.firefox", "Firefox", App.State.INSTALLED, null)) +} + diff --git a/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt similarity index 75% rename from vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt rename to vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index 32c74e14a9..2df729e593 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/EnterpriseListComponent.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -1,17 +1,12 @@ -package org.microg.vending.ui +package org.microg.vending.ui.components -import android.util.Log -import android.widget.Space import androidx.annotation.StringRes import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -28,18 +23,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import com.android.vending.R import com.google.android.finsky.AppInstallPolicy -import org.microg.gms.ui.TAG import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp - @Composable -fun EnterpriseListComponent(apps: List) { - if (apps.isNotEmpty()) LazyColumn(Modifier.padding(16.dp)) { +fun EnterpriseList(apps: List) { + if (apps.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) { val requiredApps = apps.filter { it.policy == AppInstallPolicy.MANDATORY } if (requiredApps.isNotEmpty()) { @@ -73,7 +65,7 @@ fun EnterpriseListComponent(apps: List) { fun InListHeading(@StringRes text: Int) { Text( stringResource(text), - modifier = Modifier.padding(top = 16.dp, bottom = 8.dp), + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp), style = MaterialTheme.typography.headlineSmall ) } @@ -103,30 +95,10 @@ fun InListWarning(@StringRes text: Int) { } -@Composable -fun AppRow(app: App) { - Row( - Modifier.padding(top = 8.dp, bottom = 8.dp, end = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), - verticalAlignment = Alignment.CenterVertically - ) { - if (app.iconUrl != null) { - AsyncImage( - model = app.iconUrl, - modifier = Modifier.size(48.dp), - contentDescription = null, - ) - } else { - Spacer(Modifier.size(48.dp)) - } - Text(app.displayName) - } -} - @Preview @Composable -fun EnterpriseListComponentPreview() { - EnterpriseListComponent( +fun EnterpriseListPreview() { + EnterpriseList( listOf( EnterpriseApp("com.android.vending", "Market", App.State.INSTALLED, null, AppInstallPolicy.MANDATORY), EnterpriseApp("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, null, AppInstallPolicy.OPTIONAL), @@ -137,6 +109,6 @@ fun EnterpriseListComponentPreview() { @Preview @Composable -fun EnterpriseListComponentEmptyPreview() { - EnterpriseListComponent(emptyList()) +fun EnterpriseListEmptyPreview() { + EnterpriseList(emptyList()) } diff --git a/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt b/vending-app/src/main/java/org/microg/vending/ui/components/NetworkState.kt similarity index 79% rename from vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt rename to vending-app/src/main/java/org/microg/vending/ui/components/NetworkState.kt index 2434fe376d..f2b1c01c12 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/NetworkStateComponent.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/NetworkState.kt @@ -1,4 +1,4 @@ -package org.microg.vending.ui +package org.microg.vending.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -15,9 +15,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.vending.R +import org.microg.vending.ui.NetworkState @Composable -fun NetworkStateComponent(networkState: NetworkState, retry: () -> Unit, content: @Composable () -> Unit) { +fun NetworkState(networkState: NetworkState, retry: () -> Unit, content: @Composable () -> Unit) { when (networkState) { NetworkState.ACTIVE -> { Box(Modifier.fillMaxSize()) { @@ -48,20 +49,20 @@ fun NetworkStateComponent(networkState: NetworkState, retry: () -> Unit, content @Preview @Composable -fun NetworkStateComponentActivePreview() { - NetworkStateComponent(NetworkState.ACTIVE, { }) {} +fun NetworkStateActivePreview() { + NetworkState(NetworkState.ACTIVE, { }) {} } @Preview @Composable -fun NetworkStateComponentErrorPreview() { - NetworkStateComponent(NetworkState.ERROR, { }) {} +fun NetworkStateErrorPreview() { + NetworkState(NetworkState.ERROR, { }) {} } @Preview @Composable -fun NetworkStateComponentPassivePreview() { - NetworkStateComponent(NetworkState.PASSIVE, {}) { +fun NetworkStatePassivePreview() { + NetworkState(NetworkState.PASSIVE, {}) { Text("Network operation complete.", Modifier.padding(16.dp)) } } \ No newline at end of file diff --git a/vending-app/src/main/res/drawable/ic_download.xml b/vending-app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000000..80aafe20b1 --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/vending-app/src/main/res/drawable/ic_update.xml b/vending-app/src/main/res/drawable/ic_update.xml new file mode 100644 index 0000000000..4eba07e840 --- /dev/null +++ b/vending-app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/vending-app/src/main/res/drawable/ic_work.xml b/vending-app/src/main/res/drawable/ic_work.xml index 2f9846c48b..8315631566 100644 --- a/vending-app/src/main/res/drawable/ic_work.xml +++ b/vending-app/src/main/res/drawable/ic_work.xml @@ -1,3 +1,7 @@ + diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index b865e8b867..0e617924d7 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -26,6 +26,9 @@ No apps have been made available by your administrator Update available Installed apps + Install + Update + Uninstall Pay currently not possible From e99cbd31861a2408693da3307e597eeda4204c71 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 01:00:44 +0200 Subject: [PATCH 32/59] Fetch app download URLs --- .../gms/auth/workaccount/AuthRequest.java | 7 +- .../microg/gms/auth/workaccount/GServices.kt | 20 ++ play-services-base/core/build.gradle | 1 + .../gms/common/DeviceConfiguration.java | 2 +- .../microg/gms/common/DeviceConfigProto.kt | 21 +++ .../src/main/proto/checkin.proto | 69 +------ .../src/main/proto/deviceconfig.proto | 71 ++++++++ .../src/main/proto/uploaddeviceconfig.proto | 14 ++ .../org/microg/gms/checkin/CheckinClient.java | 19 +- vending-app/build.gradle | 1 + .../vending/billing/core/GooglePlayApi.kt | 1 + .../java/org/microg/vending/enterprise/App.kt | 10 +- .../vending/enterprise/EnterpriseApp.kt | 6 +- .../org/microg/vending/ui/VendingActivity.kt | 171 ++++++++++++------ .../microg/vending/ui/WorkVendingTopAppBar.kt | 50 +++++ .../microg/vending/ui/components/AppRow.kt | 34 ++-- .../vending/ui/components/EnterpriseList.kt | 16 +- .../kotlin/com/android/vending/extensions.kt | 21 ++- .../SplitInstallManager.kt | 3 +- .../src/main/proto/GetItemsRequest.proto | 3 + .../src/main/proto/GetItemsResponse.proto | 8 + vending-app/src/main/proto/SplitInstall.proto | 22 ++- 22 files changed, 384 insertions(+), 186 deletions(-) create mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/GServices.kt create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/common/DeviceConfigProto.kt create mode 100644 play-services-core-proto/src/main/proto/deviceconfig.proto create mode 100644 play-services-core-proto/src/main/proto/uploaddeviceconfig.proto create mode 100644 vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java index 7c9c3f577e..b4070f1274 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java @@ -9,6 +9,7 @@ import static org.microg.gms.common.HttpFormClient.RequestHeader; import android.content.Context; +import android.util.Log; import org.microg.gms.common.Constants; import org.microg.gms.common.HttpFormClient; @@ -17,6 +18,7 @@ import org.microg.gms.profile.ProfileManager; import java.io.IOException; +import java.math.BigInteger; import java.util.Locale; import java.util.Map; @@ -127,8 +129,9 @@ public AuthRequest locale(Locale locale) { public AuthRequest fromContext(Context context) { build(context); locale(Utils.getLocale(context)); - if (false) { - //androidIdHex = Long.toHexString(LastCheckinInfo.read(context).getAndroidId()); + if (true) { + androidIdHex = new BigInteger(GServices.INSTANCE.getString(context.getContentResolver(), "android_id", "0")).toString(16); + Log.d("gsf", androidIdHex); } if (false) { deviceName = ""; diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/GServices.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/GServices.kt new file mode 100644 index 0000000000..8bb4cf142b --- /dev/null +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/GServices.kt @@ -0,0 +1,20 @@ +package org.microg.gms.auth.workaccount + +import android.content.ContentResolver +import android.net.Uri + +// TODO: Deduplicate & Move +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) + cursor?.use { + if (cursor.moveToNext()) { + result = cursor.getString(1) + } + } + return result + } +} \ No newline at end of file diff --git a/play-services-base/core/build.gradle b/play-services-base/core/build.gradle index 7c00ff5592..05b10fcaee 100644 --- a/play-services-base/core/build.gradle +++ b/play-services-base/core/build.gradle @@ -10,6 +10,7 @@ apply plugin: 'signing' dependencies { api project(':play-services-basement-ktx') + implementation project(":play-services-core-proto") implementation "androidx.annotation:annotation:$annotationVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" diff --git a/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java b/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java index 05473ca559..c8faba72e6 100644 --- a/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java +++ b/play-services-base/core/src/main/java/org/microg/gms/common/DeviceConfiguration.java @@ -66,7 +66,7 @@ public DeviceConfiguration(Context context) { keyboardType = configurationInfo.reqKeyboardType; navigation = configurationInfo.reqNavigation; Configuration configuration = context.getResources().getConfiguration(); - screenLayout = configuration.screenLayout; + screenLayout = configuration.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK; hasHardKeyboard = (configurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD) > 0; hasFiveWayNavigation = (configurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV) > 0; DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/common/DeviceConfigProto.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/common/DeviceConfigProto.kt new file mode 100644 index 0000000000..797f7ef5ee --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/common/DeviceConfigProto.kt @@ -0,0 +1,21 @@ +package org.microg.gms.common + +import org.microg.gms.checkin.DeviceConfig + +fun DeviceConfiguration.asProto(): DeviceConfig = DeviceConfig( + availableFeature = availableFeatures, + densityDpi = densityDpi, + glEsVersion = glEsVersion, + glExtension = glExtensions, + hasFiveWayNavigation = hasFiveWayNavigation, + hasHardKeyboard = hasHardKeyboard, + heightPixels = heightPixels, + keyboardType = keyboardType, + locale = locales, + nativePlatform = nativePlatforms, + navigation = navigation, + screenLayout = screenLayout, + sharedLibrary = sharedLibraries, + touchScreen = touchScreen, + widthPixels = widthPixels +) \ No newline at end of file diff --git a/play-services-core-proto/src/main/proto/checkin.proto b/play-services-core-proto/src/main/proto/checkin.proto index 2e0b75f311..96cf9b73cc 100644 --- a/play-services-core-proto/src/main/proto/checkin.proto +++ b/play-services-core-proto/src/main/proto/checkin.proto @@ -2,6 +2,8 @@ option java_package = "org.microg.gms.checkin"; option java_outer_classname = "CheckinProto"; +import "deviceconfig.proto"; + // Sample data, if provided, is fished from a Nexus 7 (2013) / flo running Android 5.0 message CheckinRequest { // unused @@ -156,73 +158,6 @@ message CheckinRequest { optional string esn = 17; optional DeviceConfig deviceConfiguration = 18; - message DeviceConfig { - // ConfigurationInfo.reqTouchScreen - // eg. 3 - optional int32 touchScreen = 1; - - // ConfigurationInfo.reqKeyboardType - // eg. 1 - optional int32 keyboardType = 2; - - // ConfigurationInfo.reqNavigation - // eg. 1 - optional int32 navigation = 3; - // ConfigurationInfo.screenLayout - // eg. 3 - optional int32 screenLayout = 4; - - // ConfigurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD - // eg. 0 - optional bool hasHardKeyboard = 5; - - // ConfigurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV - // eg. 0 - optional bool hasFiveWayNavigation = 6; - - // DisplayMetrics.densityDpi - // eg. 320 - optional int32 densityDpi = 7; - - // ConfigurationInfo.reqGlEsVersion - // eg. 196608 - optional int32 glEsVersion = 8; - - // PackageManager.getSystemSharedLibraryNames - // eg. "android.test.runner", "com.android.future.usb.accessory", "com.android.location.provider", - // "com.android.media.remotedisplay", "com.android.mediadrm.signer", "com.google.android.maps", - // "com.google.android.media.effects", "com.google.widevine.software.drm", "javax.obex" - repeated string sharedLibrary = 9; - - // PackageManager.getSystemAvailableFeatures - // eg. android.hardware.[...] - repeated string availableFeature = 10; - - // Build.CPU_ABI and Build.CPU_ABI2 != "unknown" - // eg. "armeabi-v7a", "armeabi" - repeated string nativePlatform = 11; - - // DisplayMetrics.widthPixels - // eg. 1200 - optional int32 widthPixels = 12; - - // DisplayMetrics.heightPixels - // eg. 1824 - optional int32 heightPixels = 13; - - // Context.getAssets.getLocales - // eg. [...], "en-US", [...] - repeated string locale = 14; - - // GLES10.glGetString(GLES10.GL_EXTENSIONS) - // eg. "GL_AMD_compressed_ATC_texture", [...] - repeated string glExtension = 15; - - // unused - optional int32 deviceClass = 16; - // unused - optional int32 maxApkDownloadSizeMb = 17; - } // "ethernet" or "wifi" repeated string macAddressType = 19; diff --git a/play-services-core-proto/src/main/proto/deviceconfig.proto b/play-services-core-proto/src/main/proto/deviceconfig.proto new file mode 100644 index 0000000000..f3e8a864e1 --- /dev/null +++ b/play-services-core-proto/src/main/proto/deviceconfig.proto @@ -0,0 +1,71 @@ +option java_package = "org.microg.gms.checkin"; +option java_outer_classname = "DeviceConfig"; + + +message DeviceConfig { + // ConfigurationInfo.reqTouchScreen + // eg. 3 + optional int32 touchScreen = 1; + + // ConfigurationInfo.reqKeyboardType + // eg. 1 + optional int32 keyboardType = 2; + + // ConfigurationInfo.reqNavigation + // eg. 1 + optional int32 navigation = 3; + // ConfigurationInfo.screenLayout + // eg. 3 + optional int32 screenLayout = 4; + + // ConfigurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD + // eg. 0 + optional bool hasHardKeyboard = 5; + + // ConfigurationInfo.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV + // eg. 0 + optional bool hasFiveWayNavigation = 6; + + // DisplayMetrics.densityDpi + // eg. 320 + optional int32 densityDpi = 7; + + // ConfigurationInfo.reqGlEsVersion + // eg. 196608 + optional int32 glEsVersion = 8; + + // PackageManager.getSystemSharedLibraryNames + // eg. "android.test.runner", "com.android.future.usb.accessory", "com.android.location.provider", + // "com.android.media.remotedisplay", "com.android.mediadrm.signer", "com.google.android.maps", + // "com.google.android.media.effects", "com.google.widevine.software.drm", "javax.obex" + repeated string sharedLibrary = 9; + + // PackageManager.getSystemAvailableFeatures + // eg. android.hardware.[...] + repeated string availableFeature = 10; + + // Build.CPU_ABI and Build.CPU_ABI2 != "unknown" + // eg. "armeabi-v7a", "armeabi" + repeated string nativePlatform = 11; + + // DisplayMetrics.widthPixels + // eg. 1200 + optional int32 widthPixels = 12; + + // DisplayMetrics.heightPixels + // eg. 1824 + optional int32 heightPixels = 13; + + // Context.getAssets.getLocales + // eg. [...], "en-US", [...] + repeated string locale = 14; + + // GLES10.glGetString(GLES10.GL_EXTENSIONS) + // eg. "GL_AMD_compressed_ATC_texture", [...] + repeated string glExtension = 15; + + // unused + optional int32 deviceClass = 16; + // unused + optional int32 maxApkDownloadSizeMb = 17; +} diff --git a/play-services-core-proto/src/main/proto/uploaddeviceconfig.proto b/play-services-core-proto/src/main/proto/uploaddeviceconfig.proto new file mode 100644 index 0000000000..0c5d858cb3 --- /dev/null +++ b/play-services-core-proto/src/main/proto/uploaddeviceconfig.proto @@ -0,0 +1,14 @@ + +// This should be part of the vending package, but it is hard to import proto +// files from other modules. + +option java_package = "org.microg.vending"; +option java_outer_classname = "UploadDeviceConfigRequest"; + +import "deviceconfig.proto"; + +message UploadDeviceConfigRequest { + optional DeviceConfig deviceConfiguration = 1; + optional string manufacturer = 2; + optional string gcmRegistrationId = 3; +} \ No newline at end of file diff --git a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinClient.java b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinClient.java index 2374d25d6e..e101907d31 100644 --- a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinClient.java +++ b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinClient.java @@ -19,6 +19,7 @@ import android.content.Context; import android.util.Log; +import org.microg.gms.common.DeviceConfigProtoKt; import org.microg.gms.common.DeviceConfiguration; import org.microg.gms.common.DeviceIdentifier; import org.microg.gms.common.PhoneInfo; @@ -116,23 +117,7 @@ public static CheckinRequest makeRequest(Context context, DeviceConfiguration de .stat(TODO_LIST_CHECKIN) .userNumber(0) .build()) - .deviceConfiguration(new CheckinRequest.DeviceConfig.Builder() - .availableFeature(deviceConfiguration.availableFeatures) - .densityDpi(deviceConfiguration.densityDpi) - .glEsVersion(deviceConfiguration.glEsVersion) - .glExtension(deviceConfiguration.glExtensions) - .hasFiveWayNavigation(deviceConfiguration.hasFiveWayNavigation) - .hasHardKeyboard(deviceConfiguration.hasHardKeyboard) - .heightPixels(deviceConfiguration.heightPixels) - .keyboardType(deviceConfiguration.keyboardType) - .locale(deviceConfiguration.locales) - .nativePlatform(deviceConfiguration.nativePlatforms) - .navigation(deviceConfiguration.navigation) - .screenLayout(deviceConfiguration.screenLayout & 0xF) - .sharedLibrary(deviceConfiguration.sharedLibraries) - .touchScreen(deviceConfiguration.touchScreen) - .widthPixels(deviceConfiguration.widthPixels) - .build()) + .deviceConfiguration(DeviceConfigProtoKt.asProto(deviceConfiguration)) .digest(checkinInfo.getDigest()) .esn(deviceIdent.esn) .fragment(0) diff --git a/vending-app/build.gradle b/vending-app/build.gradle index ac6bd6f35c..b3cceb42d2 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -91,6 +91,7 @@ dependencies { implementation project(':fake-signature') implementation project(':play-services-auth') implementation project(':play-services-base-core') + implementation project(':play-services-core-proto') implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.squareup.wire:wire-grpc-client:$wireVersion" diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt index 54e92507da..35322d7e52 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/GooglePlayApi.kt @@ -13,6 +13,7 @@ class GooglePlayApi { const val URL_DETAILS = "$URL_FDFE/details" const val URL_ITEM_DETAILS = "$URL_FDFE/getItems" const val URL_PURCHASE = "$URL_FDFE/purchase" + const val URL_DELIVERY = "$URL_FDFE/delivery" const val URL_ENTERPRISE_CLIENT_POLICY = "$URL_FDFE/getEnterpriseClientPolicy" } } \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt index 744be08abf..98d0d9e20c 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt @@ -2,13 +2,19 @@ package org.microg.vending.enterprise open class App( val packageName: String, + val versionCode: Int?, val displayName: String, val state: State, - val iconUrl: String? + val iconUrl: String?, + val deliveryToken: String? ) { enum class State { /** - * App is available, but not installed on the user's device. + * App cannot be installed on this user's device + */ + NOT_COMPATIBLE, + /** + * App is available, but not installed on the user's device. */ NOT_INSTALLED, /** diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt index d31169e07e..5dedc7b2ab 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt @@ -4,8 +4,10 @@ import com.google.android.finsky.AppInstallPolicy class EnterpriseApp( packageName: String, + versionCode: Int?, displayName: String, state: State, iconUrl: String?, - val policy: AppInstallPolicy, -) : App(packageName, displayName, state, iconUrl) \ No newline at end of file + deliveryToken: String?, + val policy: AppInstallPolicy +) : App(packageName, versionCode, displayName, state, iconUrl, deliveryToken) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index d52483dec8..6987b9aaee 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -1,45 +1,46 @@ package org.microg.vending.ui -import AppMeta -import GetItemsRequest -import GetItemsResponse -import RequestApp -import RequestItem +import android.accounts.Account import android.accounts.AccountManager import android.os.Bundle import android.util.Log +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.android.vending.R +import com.android.vending.AppMeta +import com.android.vending.GetItemsRequest +import com.android.vending.GetItemsResponse +import com.android.vending.RequestApp +import com.android.vending.RequestItem import com.android.vending.buildRequestHeaders import com.android.volley.VolleyError import com.google.android.finsky.GoogleApiResponse +import com.google.android.finsky.splitinstallservice.SplitInstallManager import kotlinx.coroutines.runBlocking +import org.microg.gms.common.DeviceConfiguration +import org.microg.gms.common.asProto +import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager import org.microg.gms.ui.TAG +import org.microg.vending.UploadDeviceConfigRequest import org.microg.vending.billing.AuthManager +import org.microg.vending.billing.core.AuthData +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_FDFE import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo @@ -49,25 +50,72 @@ import org.microg.vending.ui.components.EnterpriseList import org.microg.vending.ui.components.NetworkState import java.io.IOException - +@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) class VendingActivity : ComponentActivity() { var apps: MutableList = mutableStateListOf() var networkState by mutableStateOf(NetworkState.ACTIVE) - @OptIn(ExperimentalMaterial3Api::class) + var auth: AuthData? = null + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) ProfileManager.ensureInitialized(this) - val am = AccountManager.get(this) - val account = am.getAccountsByType("com.google.work").first()!! + val accountManager = AccountManager.get(this) + val accounts = accountManager.getAccountsByType("com.google.work") + if (accounts.isEmpty()) { + TODO("App should only be visible if work accounts are added. Disable component and wonder why it was enabled in the first place") + } else if (accounts.size > 1) { + Log.w(TAG, "Multiple work accounts found. This is unexpected and could point " + + "towards misuse of the work account service API by the DPC.") + } + val account = accounts.first() + + load(account) + + val install: (app: EnterpriseApp) -> Unit = { app -> + Toast.makeText(this, "installing ${app.displayName} / ${app.packageName}", Toast.LENGTH_SHORT).show() + Thread { + runBlocking { + // Get download links for requested package + val res = HttpClient(this@VendingActivity).get( + url = URL_DELIVERY, + headers = buildRequestHeaders(auth!!.authToken, auth!!.gsfId.toLong(16)), + params = mapOf( + "ot" to "1", + "doc" to app.packageName, + "vc" to app.versionCode!!.toString() + ).plus(app.deliveryToken?.let { listOf("dtok" to it) } ?: emptyList()), + adapter = GoogleApiResponse.ADAPTER + ) + + Log.d(TAG, res.toString()) + // TODO: install + //SplitInstallManager(this@VendingActivity).startInstall(app.packageName, emptyList(), app.deliveryToken) + } + }.start() + } + + val uninstall: (app: EnterpriseApp) -> Unit = { + TODO("uninstallation not yet implemented") + } + + setContent { + VendingUi(account, install, uninstall) + } + } + + private fun load(account: Account) { + networkState = NetworkState.ACTIVE Thread { runBlocking { try { - val authData = AuthManager.getAuthData(this@VendingActivity, account) + // Authenticate + auth = AuthManager.getAuthData(this@VendingActivity, account) + val authData = auth val deviceInfo = createDeviceEnvInfo(this@VendingActivity) if (deviceInfo == null || authData == null) { Log.e(TAG, "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData") @@ -78,6 +126,20 @@ class VendingActivity : ComponentActivity() { val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) val client = HttpClient(this@VendingActivity) + // Register device for server-side compatibility checking + val upload = client.post( + url = "$URL_FDFE/uploadDeviceConfig", + headers = headers.minus("X-PS-RH"), + payload = UploadDeviceConfigRequest( + DeviceConfiguration(this@VendingActivity).asProto(), + manufacturer = Build.MANUFACTURER, + //gcmRegistrationId = TODO: looks like remote-triggered app downloads may be announced through GCM? + ), + adapter = GoogleApiResponse.ADAPTER + ) + Log.d(TAG, "uploaddc: ${upload.response!!.uploadDeviceConfigResponse}") + + // Fetch list of apps available to the scoped enterprise account val apps = client.post( url = URL_ENTERPRISE_CLIENT_POLICY, headers = headers.plus("content-type" to "application/x-protobuf"), @@ -92,24 +154,29 @@ class VendingActivity : ComponentActivity() { Log.v(TAG, "app policy: ${apps.joinToString { "${it.packageName}: ${it.policy}" }}") + // Fetch details about all available apps val details = client.post( url = URL_ITEM_DETAILS, // TODO: meaning unclear, but returns 400 without. constant? possibly has influence on which fields are returned? - headers = headers.plus("x-dfe-item-field-mask" to "GgJGCCIKBgIAXASAAAAAAQ"), + headers = headers.plus("x-dfe-item-field-mask" to "GgWGHay3ByILPP/Avy+4A4YlCRM"), adapter = GetItemsResponse.ADAPTER, payload = GetItemsRequest( apps.map { - RequestItem(RequestApp(AppMeta(it.packageName!!))) + RequestItem(RequestApp(AppMeta(it.packageName))) } ) ).items.map { it.response }.map { item -> EnterpriseApp( item!!.meta!!.packageName!!, + item.offer?.version?.versionCode, item.detail!!.name!!.displayName!!, - App.State.NOT_INSTALLED, + if (item.offer?.delivery == null) App.State.NOT_COMPATIBLE else App.State.NOT_INSTALLED, item.detail.icon?.icon?.paint?.url, + item.offer?.delivery?.key, apps.find { it.packageName!! == item.meta!!.packageName }!!.policy!!, ) + }.onEach { + Log.v(TAG, "${it.packageName} delivery token: ${it.deliveryToken ?: "none acquired"}") } this@VendingActivity.apps.apply { @@ -119,49 +186,41 @@ class VendingActivity : ComponentActivity() { networkState = NetworkState.PASSIVE } catch (e: IOException) { networkState = NetworkState.ERROR + Log.e(TAG, "Network error: ${e.message}") + e.printStackTrace() } catch (e: VolleyError) { networkState = NetworkState.ERROR + Log.e(TAG, "Network error: ${e.message}") + e.printStackTrace() } catch (e: NullPointerException) { networkState = NetworkState.ERROR + Log.e(TAG, "Unexpected network response, cannot process") + e.printStackTrace() } } }.start() - setContent { - MaterialTheme { - Scaffold( - topBar = { - TopAppBar( - title = { - Row { - Icon( - painterResource(R.drawable.ic_work), - contentDescription = null, - Modifier.align(Alignment.CenterVertically), - tint = LocalContentColor.current - ) - Text(stringResource(R.string.vending_activity_name), - Modifier - .align(Alignment.CenterVertically) - .padding(start = 8.dp) - ) - } - }, - colors = TopAppBarDefaults.smallTopAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary - ) - ) - } - ) { innerPadding -> - Column(Modifier.padding(innerPadding)) { - NetworkState(networkState, { TODO("reload") }) { - EnterpriseList(apps) - } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun VendingUi( + account: Account, + install: (app: EnterpriseApp) -> Unit, + uninstall: (app: EnterpriseApp) -> Unit + ) { + MaterialTheme { + Scaffold( + topBar = { + WorkVendingTopAppBar() + } + ) { innerPadding -> + Column(Modifier.padding(innerPadding)) { + NetworkState(networkState, { load(account) }) { + EnterpriseList(apps, install, uninstall) } } } } - } } \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt b/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt new file mode 100644 index 0000000000..5a07b984f6 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt @@ -0,0 +1,50 @@ +package org.microg.vending.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.android.vending.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WorkVendingTopAppBar() = TopAppBar( + title = { + Row { + Icon( + painterResource(R.drawable.ic_work), + contentDescription = null, + Modifier.align(Alignment.CenterVertically), + tint = LocalContentColor.current + ) + Text( + stringResource(R.string.vending_activity_name), + Modifier + .align(Alignment.CenterVertically) + .padding(start = 8.dp) + ) + } + }, + colors = TopAppBarDefaults.smallTopAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary + ) +) + +@Preview +@Composable +fun PreviewWorkVendingTopAppBar() { + WorkVendingTopAppBar() +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt index 40d986a8db..3728fcd86b 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt @@ -1,17 +1,13 @@ package org.microg.vending.ui.components -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -21,7 +17,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -31,7 +26,7 @@ import com.android.vending.R import org.microg.vending.enterprise.App @Composable -fun AppRow(app: App) { +fun AppRow(app: App, install: () -> Unit, uninstall: () -> Unit) { Row( Modifier.padding(top = 8.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), @@ -48,39 +43,50 @@ fun AppRow(app: App) { Spacer(iconSpace) } Text(app.displayName) + Spacer(Modifier.weight(1f)) - if (app.state != App.State.NOT_INSTALLED) { - IconButton({}) { + if (app.state == App.State.NOT_COMPATIBLE) { + Icon(Icons.Default.Warning, null, Modifier.padding(end=8.dp), tint = MaterialTheme.colorScheme.secondary) + // TODO better UI + } + if (app.state == App.State.UPDATE_AVAILABLE || app.state == App.State.INSTALLED) { + IconButton(uninstall) { Icon(Icons.Default.Delete, stringResource(R.string.vending_overview_row_action_uninstall), tint = MaterialTheme.colorScheme.secondary) } } if (app.state == App.State.UPDATE_AVAILABLE) { - FilledIconButton({}, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { Icon(painterResource(R.drawable.ic_update), stringResource(R.string.vending_overview_row_action_update), tint = MaterialTheme.colorScheme.secondary) } } if (app.state == App.State.NOT_INSTALLED) - FilledIconButton({}, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary) } } } +@Preview +@Composable +fun AppRowNotCompatiblePreview() { + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_COMPATIBLE, null, null), {}, {}) +} + @Preview @Composable fun AppRowNotInstalledPreview() { - AppRow(App("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, null)) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, ""), {}, {}) } @Preview @Composable fun AppRowUpdateablePreview() { - AppRow(App("org.mozilla.firefox", "Firefox", App.State.UPDATE_AVAILABLE, null)) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.UPDATE_AVAILABLE, null, ""), {}, {}) } @Preview @Composable fun AppRowInstalledPreview() { - AppRow(App("org.mozilla.firefox", "Firefox", App.State.INSTALLED, null)) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.INSTALLED, null, ""), {}, {}) } diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index 2df729e593..98cbcbbc17 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -30,20 +30,20 @@ import org.microg.vending.enterprise.EnterpriseApp @Composable -fun EnterpriseList(apps: List) { +fun EnterpriseList(apps: List, install: (app: EnterpriseApp) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { if (apps.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) { val requiredApps = apps.filter { it.policy == AppInstallPolicy.MANDATORY } if (requiredApps.isNotEmpty()) { item { InListHeading(R.string.vending_overview_enterprise_row_mandatory) } item { InListWarning(R.string.vending_overview_enterprise_row_mandatory_hint) } - items(requiredApps) { AppRow(it) } + items(requiredApps) { AppRow(it, { install(it) }, { uninstall(it) }) } } val optionalApps = apps.filter { it.policy == AppInstallPolicy.OPTIONAL } if (optionalApps.isNotEmpty()) { item { InListHeading(R.string.vending_overview_enterprise_row_offered) } - items(optionalApps) { AppRow(it) } + items(optionalApps) { AppRow(it, { install(it) }, { uninstall(it) }) } } } else Box( @@ -100,15 +100,15 @@ fun InListWarning(@StringRes text: Int) { fun EnterpriseListPreview() { EnterpriseList( listOf( - EnterpriseApp("com.android.vending", "Market", App.State.INSTALLED, null, AppInstallPolicy.MANDATORY), - EnterpriseApp("org.mozilla.firefox", "Firefox", App.State.NOT_INSTALLED, null, AppInstallPolicy.OPTIONAL), - EnterpriseApp("org.thoughtcrime.securesms", "Signal", App.State.NOT_INSTALLED, null, AppInstallPolicy.OPTIONAL) - ) + EnterpriseApp("com.android.vending", 0, "Market", App.State.INSTALLED, null, "", AppInstallPolicy.MANDATORY), + EnterpriseApp("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, "", AppInstallPolicy.OPTIONAL), + EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", App.State.NOT_COMPATIBLE, null, "", AppInstallPolicy.OPTIONAL) + ), {}, {} ) } @Preview @Composable fun EnterpriseListEmptyPreview() { - EnterpriseList(emptyList()) + EnterpriseList(emptyList(), {}, {}) } diff --git a/vending-app/src/main/kotlin/com/android/vending/extensions.kt b/vending-app/src/main/kotlin/com/android/vending/extensions.kt index 34fee97b25..12095facc2 100644 --- a/vending-app/src/main/kotlin/com/android/vending/extensions.kt +++ b/vending-app/src/main/kotlin/com/android/vending/extensions.kt @@ -14,6 +14,7 @@ import android.util.Log import com.google.android.gms.common.BuildConfig import okio.ByteString import org.microg.gms.profile.Build +import org.microg.vending.billing.getUserAgent import java.io.ByteArrayOutputStream import java.io.IOException import java.net.URLEncoder @@ -78,18 +79,18 @@ fun buildRequestHeaders(auth: String, androidId: Long, language: List ?= 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") + val userAgent = getUserAgent() return mapOf( - "X-PS-RH" to xPsRh, "User-Agent" to userAgent, "Authorization" to "Bearer $auth", "Accept-Language" to "en-US", "Connection" to "Keep-Alive" + "X-PS-RH" to xPsRh, + "User-Agent" to userAgent, + "Authorization" to "Bearer $auth", + "Accept-Language" to "en-US", + "Connection" to "Keep-Alive", + "X-DFE-Device-Id" to androidId.toBigInteger().toString(16), + "X-DFE-Client-Id" to "am-google", + "X-DFE-Encoded-Targets" to "CAESN/qigQYC2AMBFfUbyA7SM5Ij/CvfBoIDgxHqGP8R3xzIBvoQtBKFDZ4HAY4FrwSVMasHBO0O2Q8akgYRAQECAQO7AQEpKZ0CnwECAwRrAQYBr9PPAoK7sQMBAQMCBAkIDAgBAwEDBAICBAUZEgMEBAMLAQEBBQEBAcYBARYED+cBfS8CHQEKkAEMMxcBIQoUDwYHIjd3DQ4MFk0JWGYZEREYAQOLAYEBFDMIEYMBAgICAgICOxkCD18LGQKEAcgDBIQBAgGLARkYCy8oBTJlBCUocxQn0QUBDkkGxgNZQq0BZSbeAmIDgAEBOgGtAaMCDAOQAZ4BBIEBKUtQUYYBQscDDxPSARA1oAEHAWmnAsMB2wFyywGLAxol+wImlwOOA80CtwN26A0WjwJVbQEJPAH+BRDeAfkHK/ABASEBCSAaHQemAzkaRiu2Ad8BdXeiAwEBGBUBBN4LEIABK4gB2AFLfwECAdoENq0CkQGMBsIBiQEtiwGgA1zyAUQ4uwS8AwhsvgPyAcEDF27vApsBHaICGhl3GSKxAR8MC6cBAgItmQYG9QIeywLvAeYBDArLAh8HASI4ELICDVmVBgsY/gHWARtcAsMBpALiAdsBA7QBpAJmIArpByn0AyAKBwHTARIHAX8D+AMBcRIBBbEDmwUBMacCHAciNp0BAQF0OgQLJDuSAh54kwFSP0eeAQQ4M5EBQgMEmwFXywFo0gFyWwMcapQBBugBPUW2AVgBKmy3AR6PAbMBGQxrUJECvQR+8gFoWDsYgQNwRSczBRXQAgtRswEW0ALMAREYAUEBIG6yATYCRE8OxgER8gMBvQEDRkwLc8MBTwHZAUOnAXiiBakDIbYBNNcCIUmuArIBSakBrgFHKs0EgwV/G3AD0wE6LgECtQJ4xQFwFbUCjQPkBS6vAQqEAUZF3QIM9wEhCoYCQhXsBCyZArQDugIziALWAdIBlQHwBdUErQE6qQaSA4EEIvYBHir9AQVLmgMCApsCKAwHuwgrENsBAjNYswEVmgIt7QJnN4wDEnta+wGfAcUBxgEtEFXQAQWdAUAeBcwBAQM7rAEJATJ0LENrdh73A6UBhAE+qwEeASxLZUMhDREuH0CGARbd7K0GlQo", + "X-DFE-Phenotype" to "H4sIAAAAAAAAAB3OO3KjMAAA0KRNuWXukBkBQkAJ2MhgAZb5u2GCwQZbCH_EJ77QHmgvtDtbv-Z9_H63zXXU0NVPB1odlyGy7751Q3CitlPDvFd8lxhz3tpNmz7P92CFw73zdHU2Ie0Ad2kmR8lxhiErTFLt3RPGfJQHSDy7Clw10bg8kqf2owLokN4SecJTLoSwBnzQSd652_MOf2d1vKBNVedzg4ciPoLz2mQ8efGAgYeLou-l-PXn_7Sna1MfhHuySxt-4esulEDp8Sbq54CPPKjpANW-lkU2IZ0F92LBI-ukCKSptqeq1eXU96LD9nZfhKHdtjSWwJqUm_2r6pMHOxk01saVanmNopjX3YxQafC4iC6T55aRbC8nTI98AF_kItIQAJb5EQxnKTO7TZDWnr01HVPxelb9A2OWX6poidMWl16K54kcu_jhXw-JSBQkVcD_fPsLSZu6joIBAAA" ) } 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 aa92d5ef7c..cb072c78d2 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 @@ -38,6 +38,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY import org.microg.vending.billing.core.HttpClient import java.io.File import java.io.FileInputStream @@ -180,7 +181,7 @@ class SplitInstallManager(val context: Context) { 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=") + StringBuilder("$URL_DELIVERY?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=") 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, "") } diff --git a/vending-app/src/main/proto/GetItemsRequest.proto b/vending-app/src/main/proto/GetItemsRequest.proto index ae76ed7f8e..c9659f6154 100644 --- a/vending-app/src/main/proto/GetItemsRequest.proto +++ b/vending-app/src/main/proto/GetItemsRequest.proto @@ -1,5 +1,8 @@ syntax = "proto3"; +option java_package = "com.android.vending"; +option java_multiple_files = true; + message GetItemsRequest { repeated RequestItem items = 2; } diff --git a/vending-app/src/main/proto/GetItemsResponse.proto b/vending-app/src/main/proto/GetItemsResponse.proto index ad2ab7a007..c644a4703c 100644 --- a/vending-app/src/main/proto/GetItemsResponse.proto +++ b/vending-app/src/main/proto/GetItemsResponse.proto @@ -1,6 +1,8 @@ syntax = "proto3"; import "GetItemsRequest.proto"; +option java_package = "com.android.vending"; +option java_multiple_files = true; message GetItemsResponse { // technically this is a typical Google response containing the typical `GoogleApiResponse` fields, @@ -41,9 +43,15 @@ message IconPaint { } message ItemOffer { + optional ItemVersion version = 2; optional ItemDelivery delivery = 28; } +message ItemVersion { + optional int32 versionCode = 1; + optional int32 versionName = 2; +} + message ItemDelivery { optional string key = 14; } diff --git a/vending-app/src/main/proto/SplitInstall.proto b/vending-app/src/main/proto/SplitInstall.proto index cb2f5ea491..48b1c8eb4a 100644 --- a/vending-app/src/main/proto/SplitInstall.proto +++ b/vending-app/src/main/proto/SplitInstall.proto @@ -15,28 +15,38 @@ message UnknownType { } message ApiResponse { - optional TocResponse tocApi = 6; optional SplitResponse splitReqResult = 21; + optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 28; optional EnterpriseClientPolicyResponse enterpriseClientPolicyResult = 135; // optional SyncApiResp syncResult = 183; } -message TocResponse { - optional string tocTokenValue = 22; +message UploadDeviceConfigResponse { + optional string deviceConfigToken = 1; } message SplitResponse { - optional int32 unknownInt32 = 1; + optional DeliveryStatus status = 1; optional PkgFetchInfo pkgList = 2; } +enum DeliveryStatus { + SUCCESS = 1; + NOT_SUPPORTED = 2; + NOT_PURCHASED = 3; + APP_REMOVED = 7; + APP_NOT_SUPPORTED = 9; +} + message PkgFetchInfo { + optional uint32 baseBytes = 1; + optional string baseUrl = 3; repeated SplitPkgInfo pkgDownLoadInfo = 15; } message SplitPkgInfo { optional string splitPkgName = 1; - optional int64 size = 2; + optional uint32 size = 2; optional string checkSum = 4; optional string downloadUrl = 5; optional DownloadInfo slaveDownloadInfo = 8; @@ -47,7 +57,7 @@ message SplitPkgInfo { message DownloadInfo { optional int32 id = 1; - optional int64 size = 2; + optional uint32 bytes = 2; optional string url = 3; } From 80c9c52198adf10a2332734840fe749fc26ac050 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 16:24:33 +0200 Subject: [PATCH 33/59] Add app installation support --- vending-app/src/main/AndroidManifest.xml | 2 + .../org/microg/vending/ui/VendingActivity.kt | 47 +++++++++++++++---- .../microg/vending/ui/components/AppRow.kt | 12 ++--- .../vending/ui/components/EnterpriseList.kt | 10 ++-- .../SplitInstallManager.kt | 17 +++++-- 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 023cc56063..e0a41575b8 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ + + Unit = { app -> + val install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit = { app, isUpdate -> Toast.makeText(this, "installing ${app.displayName} / ${app.packageName}", Toast.LENGTH_SHORT).show() Thread { runBlocking { @@ -93,8 +93,20 @@ class VendingActivity : ComponentActivity() { ) Log.d(TAG, res.toString()) - // TODO: install - //SplitInstallManager(this@VendingActivity).startInstall(app.packageName, emptyList(), app.deliveryToken) + val triples = setOf(Triple("base", res.response!!.splitReqResult!!.pkgList!!.baseUrl!!, 0)) + + res.response!!.splitReqResult!!.pkgList!!.pkgDownLoadInfo!!.map { + Triple(it.splitPkgName!!, it.downloadUrl!!, 0) + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + SplitInstallManager(this@VendingActivity).apply { + triples.forEach { updateSplitInstallRecord(app.packageName, it) } + notify(this@VendingActivity) + installSplitPackage(this@VendingActivity, app.packageName, triples, isUpdate) + } + } else { + TODO("implement installation on Lollipop devices") + } } }.start() } @@ -165,15 +177,32 @@ class VendingActivity : ComponentActivity() { RequestItem(RequestApp(AppMeta(it.packageName))) } ) - ).items.map { it.response }.map { item -> + ).items.map { it.response }.filterNotNull().map { item -> + val packageName = item.meta!!.packageName!! + val installedDetails = this@VendingActivity.packageManager.getInstalledPackages(0).find { + it.applicationInfo.packageName == packageName + } + + val available = item.offer?.delivery != null + + val versionCode = if (available) { + item.offer!!.version!!.versionCode!! + } else null + + val state = if (!available && installedDetails == null) App.State.NOT_COMPATIBLE + else if (!available && installedDetails != null) App.State.INSTALLED + else if (available && installedDetails == null) App.State.NOT_INSTALLED + else if (available && installedDetails != null && installedDetails.versionCode > versionCode!!) App.State.UPDATE_AVAILABLE + else /* if (available && installedDetails != null) */ App.State.INSTALLED + EnterpriseApp( - item!!.meta!!.packageName!!, - item.offer?.version?.versionCode, + packageName, + versionCode, item.detail!!.name!!.displayName!!, - if (item.offer?.delivery == null) App.State.NOT_COMPATIBLE else App.State.NOT_INSTALLED, + state, item.detail.icon?.icon?.paint?.url, item.offer?.delivery?.key, - apps.find { it.packageName!! == item.meta!!.packageName }!!.policy!!, + apps.find { it.packageName!! == item.meta.packageName }!!.policy!!, ) }.onEach { Log.v(TAG, "${it.packageName} delivery token: ${it.deliveryToken ?: "none acquired"}") @@ -206,7 +235,7 @@ class VendingActivity : ComponentActivity() { @Composable fun VendingUi( account: Account, - install: (app: EnterpriseApp) -> Unit, + install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit ) { MaterialTheme { diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt index 3728fcd86b..03ec321a22 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt @@ -26,7 +26,7 @@ import com.android.vending.R import org.microg.vending.enterprise.App @Composable -fun AppRow(app: App, install: () -> Unit, uninstall: () -> Unit) { +fun AppRow(app: App, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) { Row( Modifier.padding(top = 8.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), @@ -55,7 +55,7 @@ fun AppRow(app: App, install: () -> Unit, uninstall: () -> Unit) { } } if (app.state == App.State.UPDATE_AVAILABLE) { - FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + FilledIconButton(update, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { Icon(painterResource(R.drawable.ic_update), stringResource(R.string.vending_overview_row_action_update), tint = MaterialTheme.colorScheme.secondary) } } @@ -69,24 +69,24 @@ fun AppRow(app: App, install: () -> Unit, uninstall: () -> Unit) { @Preview @Composable fun AppRowNotCompatiblePreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_COMPATIBLE, null, null), {}, {}) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_COMPATIBLE, null, null), {}, {}, {}) } @Preview @Composable fun AppRowNotInstalledPreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, ""), {}, {}) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, ""), {}, {}, {}) } @Preview @Composable fun AppRowUpdateablePreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.UPDATE_AVAILABLE, null, ""), {}, {}) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.UPDATE_AVAILABLE, null, ""), {}, {}, {}) } @Preview @Composable fun AppRowInstalledPreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.INSTALLED, null, ""), {}, {}) + AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.INSTALLED, null, ""), {}, {}, {}) } diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index 98cbcbbc17..62448300af 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -30,20 +30,20 @@ import org.microg.vending.enterprise.EnterpriseApp @Composable -fun EnterpriseList(apps: List, install: (app: EnterpriseApp) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { +fun EnterpriseList(apps: List, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { if (apps.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) { val requiredApps = apps.filter { it.policy == AppInstallPolicy.MANDATORY } if (requiredApps.isNotEmpty()) { item { InListHeading(R.string.vending_overview_enterprise_row_mandatory) } item { InListWarning(R.string.vending_overview_enterprise_row_mandatory_hint) } - items(requiredApps) { AppRow(it, { install(it) }, { uninstall(it) }) } + items(requiredApps) { AppRow(it, { install(it, false) }, { install(it, true) }, { uninstall(it) }) } } val optionalApps = apps.filter { it.policy == AppInstallPolicy.OPTIONAL } if (optionalApps.isNotEmpty()) { item { InListHeading(R.string.vending_overview_enterprise_row_offered) } - items(optionalApps) { AppRow(it, { install(it) }, { uninstall(it) }) } + items(optionalApps) { AppRow(it, { install(it, false) }, { install(it, true) }, { uninstall(it) }) } } } else Box( @@ -103,12 +103,12 @@ fun EnterpriseListPreview() { EnterpriseApp("com.android.vending", 0, "Market", App.State.INSTALLED, null, "", AppInstallPolicy.MANDATORY), EnterpriseApp("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, "", AppInstallPolicy.OPTIONAL), EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", App.State.NOT_COMPATIBLE, null, "", AppInstallPolicy.OPTIONAL) - ), {}, {} + ), { _, _ -> }, {} ) } @Preview @Composable fun EnterpriseListEmptyPreview() { - EnterpriseList(emptyList(), {}, {}) + EnterpriseList(emptyList(), { _, _ -> }, {}) } 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 cb072c78d2..2d08fdd94e 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 @@ -105,7 +105,7 @@ class SplitInstallManager(val context: Context) { } @RequiresApi(Build.VERSION_CODES.M) - private suspend fun installSplitPackage(context: Context, callingPackage: String, downloadList: ArraySet>): Intent { + internal suspend fun installSplitPackage(context: Context, callingPackage: String, downloadList: Set>, isUpdate: Boolean = false): Intent { Log.d(TAG, "installSplitPackage start ") if (!context.splitSaveFile().exists()) context.splitSaveFile().mkdir() val downloadSplitPackage = downloadSplitPackage(context, callingPackage, downloadList) @@ -116,7 +116,14 @@ class SplitInstallManager(val context: Context) { Log.d(TAG, "installSplitPackage downloaded success") val packageInstaller = context.packageManager.packageInstaller - val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) + val installed = context.packageManager.getInstalledPackages(0).any { + it.applicationInfo.packageName == callingPackage + } + // Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed. + val params = PackageInstaller.SessionParams( + if (!installed || isUpdate) PackageInstaller.SessionParams.MODE_FULL_INSTALL + else PackageInstaller.SessionParams.MODE_INHERIT_EXISTING + ) params.setAppPackageName(callingPackage) params.setAppLabel(callingPackage + "Subcontracting") params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) @@ -161,7 +168,7 @@ class SplitInstallManager(val context: Context) { } @RequiresApi(Build.VERSION_CODES.M) - private suspend fun downloadSplitPackage(context: Context, callingPackage: String, downloadList: ArraySet>): Boolean = + private suspend fun downloadSplitPackage(context: Context, callingPackage: String, downloadList: Set>): Boolean = coroutineScope { val results = downloadList.map { info -> Log.d(TAG, "downloadSplitPackage: $info") @@ -234,7 +241,7 @@ class SplitInstallManager(val context: Context) { } @RequiresApi(Build.VERSION_CODES.M) - private fun updateSplitInstallRecord(callingPackage: String, triple: Triple) { + internal fun updateSplitInstallRecord(callingPackage: String, triple: Triple) { splitInstallRecord[callingPackage]?.let { triples -> val find = triples.find { it.first == triple.first } find?.let { triples.remove(it) } @@ -246,7 +253,7 @@ class SplitInstallManager(val context: Context) { } } - private fun notify(context: Context) { + internal fun notify(context: Context) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { notificationManager.createNotificationChannel( From 7ab6cf1d271d7cdae0e5e67c8d6189bdfbbc7187 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 16:50:03 +0200 Subject: [PATCH 34/59] Add app uninstallation support --- vending-app/src/main/AndroidManifest.xml | 1 + .../org/microg/vending/ui/VendingActivity.kt | 3 ++- .../finsky/splitinstallservice/Uninstaller.kt | 25 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index e0a41575b8..d9de88954e 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + Unit = { - TODO("uninstallation not yet implemented") + uninstallPackage(it.packageName) } setContent { diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt new file mode 100644 index 0000000000..c4ca40647e --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt @@ -0,0 +1,25 @@ +package com.google.android.finsky.splitinstallservice + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInstaller +import androidx.annotation.RequiresApi +import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver + +class Uninstaller { +} + +@RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) +fun Context.uninstallPackage(packageName: String) { + val installer = packageManager.packageInstaller + val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) + val session = installer.createSession(sessionParams) + installer.uninstall( + packageName, PendingIntent.getBroadcast( + this, session, Intent(this, InstallResultReceiver::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ).intentSender + ) + +} \ No newline at end of file From c3398b989ab1a01aaf0bed7a6900cc0f46225ad6 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 18:31:27 +0200 Subject: [PATCH 35/59] Refactor SplitInstallManager: data types --- .../org/microg/vending/ui/VendingActivity.kt | 17 ++-- .../splitinstallservice/DownloadStatus.kt | 7 ++ .../splitinstallservice/PackageComponent.kt | 7 ++ .../SplitInstallManager.kt | 77 +++++++++---------- 4 files changed, 61 insertions(+), 47 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/DownloadStatus.kt create mode 100644 vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index f44b584db1..390f002b51 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -28,6 +28,8 @@ import com.android.vending.RequestItem import com.android.vending.buildRequestHeaders import com.android.volley.VolleyError import com.google.android.finsky.GoogleApiResponse +import com.google.android.finsky.splitinstallservice.DownloadStatus +import com.google.android.finsky.splitinstallservice.PackageComponent import com.google.android.finsky.splitinstallservice.SplitInstallManager import com.google.android.finsky.splitinstallservice.uninstallPackage import kotlinx.coroutines.runBlocking @@ -94,16 +96,19 @@ class VendingActivity : ComponentActivity() { ) Log.d(TAG, res.toString()) - val triples = setOf(Triple("base", res.response!!.splitReqResult!!.pkgList!!.baseUrl!!, 0)) + - res.response!!.splitReqResult!!.pkgList!!.pkgDownLoadInfo!!.map { - Triple(it.splitPkgName!!, it.downloadUrl!!, 0) - } + val components = listOf( + PackageComponent(app.packageName, "base", res.response!!.splitReqResult!!.pkgList!!.baseUrl!!) + ) + res.response.splitReqResult!!.pkgList!!.pkgDownLoadInfo.map { + PackageComponent(app.packageName, it.splitPkgName!!, it.downloadUrl!!) + } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { SplitInstallManager(this@VendingActivity).apply { - triples.forEach { updateSplitInstallRecord(app.packageName, it) } + components.forEach { + SplitInstallManager.splitInstallRecord[it] = DownloadStatus.PENDING + } notify(this@VendingActivity) - installSplitPackage(this@VendingActivity, app.packageName, triples, isUpdate) + installSplitPackage(this@VendingActivity, app.packageName, components, isUpdate) } } else { TODO("implement installation on Lollipop devices") diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/DownloadStatus.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/DownloadStatus.kt new file mode 100644 index 0000000000..053fb893c3 --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/DownloadStatus.kt @@ -0,0 +1,7 @@ +package com.google.android.finsky.splitinstallservice + +enum class DownloadStatus { + PENDING, + FAILED, + COMPLETE +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt new file mode 100644 index 0000000000..3be462b41e --- /dev/null +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt @@ -0,0 +1,7 @@ +package com.google.android.finsky.splitinstallservice + +data class PackageComponent( + val packageName: String, + val componentName: String, + val url: String +) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt index 2d08fdd94e..07bff90914 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 @@ -60,10 +60,6 @@ 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" @@ -91,13 +87,13 @@ class SplitInstallManager(val context: Context) { 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()) { + val components = runCatching { requestDownloadUrls(callingPackage, oauthToken, needInstallSplitPack) }.getOrNull() + Log.w(TAG, "startInstall requestDownloadUrls triples: $components") + if (components.isNullOrEmpty()) { NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return false } - val intent = runCatching { installSplitPackage(context, callingPackage, triples) }.getOrNull() + val intent = runCatching { installSplitPackage(context, callingPackage, components) }.getOrNull() NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) if (intent == null) { return false } sendCompleteBroad(context, callingPackage, intent) @@ -105,10 +101,10 @@ class SplitInstallManager(val context: Context) { } @RequiresApi(Build.VERSION_CODES.M) - internal suspend fun installSplitPackage(context: Context, callingPackage: String, downloadList: Set>, isUpdate: Boolean = false): Intent { + internal suspend fun installSplitPackage(context: Context, callingPackage: String, downloadList: List, isUpdate: Boolean = false): Intent { Log.d(TAG, "installSplitPackage start ") if (!context.splitSaveFile().exists()) context.splitSaveFile().mkdir() - val downloadSplitPackage = downloadSplitPackage(context, callingPackage, downloadList) + val downloadSplitPackage = downloadSplitPackage(context, downloadList) if (!downloadSplitPackage) { Log.w(TAG, "installSplitPackage download failed") throw RuntimeException("installSplitPackage downloadSplitPackage has error") @@ -142,8 +138,8 @@ class SplitInstallManager(val context: Context) { 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 -> + val pkgPath = File(context.splitSaveFile().toString(), item.componentName) + session.openWrite(item.componentName, 0, -1).use { outputStream -> FileInputStream(pkgPath).use { inputStream -> inputStream.copyTo(outputStream) } session.fsync(outputStream) } @@ -155,7 +151,8 @@ class SplitInstallManager(val context: Context) { val intent = Intent(context, InstallResultReceiver::class.java).apply { putExtra(KEY_BYTES_DOWNLOADED, totalDownloaded) } - val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, 0) + val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) session.commit(pendingIntent.intentSender) Log.d(TAG, "installSplitPackage session commit") return deferred.await() @@ -168,24 +165,28 @@ class SplitInstallManager(val context: Context) { } @RequiresApi(Build.VERSION_CODES.M) - private suspend fun downloadSplitPackage(context: Context, callingPackage: String, downloadList: Set>): Boolean = + private suspend fun downloadSplitPackage( + context: Context, + downloadList: List + ): 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) + runCatching { + httpClient.download(info.url, File(context.splitSaveFile().toString(), info.componentName), 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)) } + Log.w(TAG, "downloadSplitPackage url:${info.url} save:${info.componentName}", it) + }.isSuccess.also { downloadSuccessful -> + splitInstallRecord[info] = if (downloadSuccessful) DownloadStatus.COMPLETE else DownloadStatus.FAILED + } } }.awaitAll() return@coroutineScope results.all { it } } @RequiresApi(Build.VERSION_CODES.M) - private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, packs: MutableSet): ArraySet> { + private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, packs: MutableSet): MutableList { val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(callingPackage, 0)) val requestUrl = StringBuilder("$URL_DELIVERY?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=") @@ -200,18 +201,23 @@ class SplitInstallManager(val context: Context) { ) Log.d(TAG, "requestDownloadUrls end response -> $response") val splitPkgInfoList = response.response?.splitReqResult?.pkgList?.pkgDownLoadInfo ?: throw RuntimeException("splitPkgInfoList is null") - val packSet = ArraySet>() + val components: MutableList = mutableListOf(); 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)) + components.add(PackageComponent(callingPackage, it, info.downloadUrl!!)) } } - Log.d(TAG, "requestDownloadUrls end packSet -> $packSet") - return packSet.onEach { updateSplitInstallRecord(callingPackage, it) } + + Log.d(TAG, "requestDownloadUrls end -> $components") + components.forEach { + splitInstallRecord[it] = DownloadStatus.PENDING + } + + return components } private suspend fun getOauthToken(): String { @@ -236,21 +242,10 @@ class SplitInstallManager(val context: Context) { @RequiresApi(Build.VERSION_CODES.M) private fun checkSplitInstalled(callingPackage: String, splitName: String): Boolean { - if (!splitInstallRecord.containsKey(callingPackage)) return false - return splitInstallRecord[callingPackage]?.find { it.first == splitName }?.third != STATUS_UNKNOWN - } - - @RequiresApi(Build.VERSION_CODES.M) - internal 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 - } + return splitInstallRecord.keys.find { it.packageName == callingPackage && it.componentName == splitName } + ?.let { + splitInstallRecord[it] != DownloadStatus.FAILED + } ?: false } internal fun notify(context: Context) { @@ -338,8 +333,8 @@ class SplitInstallManager(val context: Context) { } companion object { - // Installation records, including subpackage name, download path, and installation status - private val splitInstallRecord = HashMap>>() + // Installation records, including (sub)package name, download path, and installation status + internal val splitInstallRecord: MutableMap = mutableMapOf() private val deferredMap = mutableMapOf>() } } From 3edeb83c4592b8fe79bc5fb064145cd36668f4d2 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 21:23:15 +0200 Subject: [PATCH 36/59] Refactor SplitInstallManager: structural pt. 1 --- .../org/microg/vending/ui/VendingActivity.kt | 4 +- .../SplitInstallManager.kt | 238 +++++++++++------- .../SplitInstallService.kt | 2 +- 3 files changed, 144 insertions(+), 100 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 390f002b51..7076e59fc3 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -107,8 +107,8 @@ class VendingActivity : ComponentActivity() { components.forEach { SplitInstallManager.splitInstallRecord[it] = DownloadStatus.PENDING } - notify(this@VendingActivity) - installSplitPackage(this@VendingActivity, app.packageName, components, isUpdate) + notify(app.packageName) + downloadAndInstall(app.packageName, components, isUpdate) } } else { TODO("implement installation on Lollipop devices") 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 07bff90914..5fd591b683 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 @@ -16,12 +16,12 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.SessionParams +import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import android.os.Bundle -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 @@ -69,62 +69,144 @@ class SplitInstallManager(val context: Context) { private var httpClient: HttpClient = HttpClient(context) - suspend fun startInstall(callingPackage: String, splits: List): Boolean { + suspend fun splitInstallFlow(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") + Log.v(TAG, "splitInstallFlow: start") + + val packagesToDownload = splits.mapNotNull { split -> + split.getString(KEY_LANGUAGE)?.let { "$SPLIT_LANGUAGE_TAG$it" } + ?: split.getString(KEY_MODULE_NAME) + }.filter { shouldDownload(callingPackage, it) } + + Log.v(TAG, "splitInstallFlow will query for these packages: $packagesToDownload") + if (packagesToDownload.isEmpty()) return false + + val oauthToken = runCatching { withContext(Dispatchers.IO) { + getOauthToken() + } }.getOrNull() + Log.v(TAG, "splitInstallFlow oauthToken: $oauthToken") if (oauthToken.isNullOrEmpty()) return false - notify(context) - val components = runCatching { requestDownloadUrls(callingPackage, oauthToken, needInstallSplitPack) }.getOrNull() - Log.w(TAG, "startInstall requestDownloadUrls triples: $components") + + notify(callingPackage) + + val components = runCatching { requestDownloadUrls(callingPackage, oauthToken, packagesToDownload) }.getOrNull() + Log.v(TAG, "splitInstallFlow requestDownloadUrls returned these components: $components") if (components.isNullOrEmpty()) { NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return false } - val intent = runCatching { installSplitPackage(context, callingPackage, components) }.getOrNull() + + val intent = downloadAndInstall(callingPackage, components) + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) if (intent == null) { return false } sendCompleteBroad(context, callingPackage, intent) return true } - @RequiresApi(Build.VERSION_CODES.M) - internal suspend fun installSplitPackage(context: Context, callingPackage: String, downloadList: List, isUpdate: Boolean = false): Intent { - Log.d(TAG, "installSplitPackage start ") + internal suspend fun downloadAndInstall(forPackage: String, downloadList: List, isUpdate: Boolean = false): Intent? { if (!context.splitSaveFile().exists()) context.splitSaveFile().mkdir() - val downloadSplitPackage = downloadSplitPackage(context, downloadList) - if (!downloadSplitPackage) { - Log.w(TAG, "installSplitPackage download failed") - throw RuntimeException("installSplitPackage downloadSplitPackage has error") + val packageFiles = downloadPackageComponents(context, downloadList) + val installFiles = packageFiles.map { + if (it.value == null) { + Log.w(TAG, "splitInstallFlow download failed, as ${it.key} was not downloaded") + throw RuntimeException("installSplitPackage downloadSplitPackage has error") + } else it.value!! } - Log.d(TAG, "installSplitPackage downloaded success") + Log.v(TAG, "splitInstallFlow downloaded success, downloaded ${installFiles.size} files") + + return runCatching { + installPackages(context, forPackage, installFiles, isUpdate) + }.getOrNull() + + } + + @RequiresApi(Build.VERSION_CODES.M) + private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, requestSplitPackages: List): List { + val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(callingPackage, 0)) + val requestUrl = + StringBuilder("$URL_DELIVERY?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode" + + "&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=") + requestSplitPackages.forEach { requestUrl.append("&mn=").append(it) } + + Log.v(TAG, "requestDownloadUrls start") + val languages = requestSplitPackages.filter { it.startsWith(SPLIT_LANGUAGE_TAG) }.map { it.replace(SPLIT_LANGUAGE_TAG, "") } + Log.d(TAG, "requestDownloadUrls languages: $languages") + + val response = httpClient.get( + url = requestUrl.toString(), + headers = buildRequestHeaders(authToken, 1, languages).onEach { Log.d(TAG, "key:${it.key} value:${it.value}") }, + adapter = GoogleApiResponse.ADAPTER + ) + Log.d(TAG, "requestDownloadUrls end response -> $response") + + val splitPackageResponses = response.response!!.splitReqResult!!.pkgList!!.pkgDownLoadInfo.filter { + !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() + } + + val components: List = splitPackageResponses.mapNotNull { info -> + requestSplitPackages.firstOrNull { + it.contains(info.splitPkgName!!) + }?.let { + PackageComponent(callingPackage, it, info.downloadUrl!!) + } + } + + Log.d(TAG, "requestDownloadUrls end -> $components") + + components.forEach { + splitInstallRecord[it] = DownloadStatus.PENDING + } + + return components + } + + @RequiresApi(Build.VERSION_CODES.M) + private suspend fun downloadPackageComponents( + context: Context, + downloadList: List + ): Map = coroutineScope { + downloadList.map { info -> + Log.d(TAG, "downloadSplitPackage: $info") + async { + info to runCatching { + val file = File(context.splitSaveFile().toString(), info.componentName) + httpClient.download( + url = info.url, + downloadFile = file, + tag = SPLIT_INSTALL_REQUEST_TAG + ) + file + }.onFailure { + Log.w(TAG, "downloadSplitPackage failed to downlaod from url:${info.url} to be saved as `${info.componentName}`", it) + }.also { + splitInstallRecord[info] = if (it.isSuccess) DownloadStatus.COMPLETE else DownloadStatus.FAILED + }.getOrNull() + } + }.awaitAll().associate { it } + } + + @RequiresApi(Build.VERSION_CODES.M) + internal suspend fun installPackages(context: Context, callingPackage: String, componentFiles: List, isUpdate: Boolean = false): Intent { + Log.v(TAG, "installPackages start") val packageInstaller = context.packageManager.packageInstaller val installed = context.packageManager.getInstalledPackages(0).any { it.applicationInfo.packageName == callingPackage } // Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed. - val params = PackageInstaller.SessionParams( - if (!installed || isUpdate) PackageInstaller.SessionParams.MODE_FULL_INSTALL - else PackageInstaller.SessionParams.MODE_INHERIT_EXISTING + val params = SessionParams( + if (!installed || isUpdate) SessionParams.MODE_FULL_INSTALL + else SessionParams.MODE_INHERIT_EXISTING ) params.setAppPackageName(callingPackage) params.setAppLabel(callingPackage + "Subcontracting") params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) + params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) try { - @SuppressLint("PrivateApi") val method = PackageInstaller.SessionParams::class.java.getDeclaredMethod( + @SuppressLint("PrivateApi") val method = SessionParams::class.java.getDeclaredMethod( "setDontKillApp", Boolean::class.javaPrimitiveType ) method.invoke(params, true) @@ -137,9 +219,9 @@ class SplitInstallManager(val context: Context) { try { sessionId = packageInstaller.createSession(params) session = packageInstaller.openSession(sessionId) - downloadList.forEach { item -> - val pkgPath = File(context.splitSaveFile().toString(), item.componentName) - session.openWrite(item.componentName, 0, -1).use { outputStream -> + componentFiles.forEach { file -> + val pkgPath = File(context.splitSaveFile(), file.name) + session.openWrite(file.name, 0, -1).use { outputStream -> FileInputStream(pkgPath).use { inputStream -> inputStream.copyTo(outputStream) } session.fsync(outputStream) } @@ -154,72 +236,17 @@ class SplitInstallManager(val context: Context) { val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) session.commit(pendingIntent.intentSender) - Log.d(TAG, "installSplitPackage session commit") + Log.d(TAG, "installPackages session commit") return deferred.await() } catch (e: IOException) { - Log.w(TAG, "Error installing split", e) + Log.w(TAG, "Error installing packages", e) throw e } finally { session?.close() } } - @RequiresApi(Build.VERSION_CODES.M) - private suspend fun downloadSplitPackage( - context: Context, - downloadList: List - ): Boolean = - coroutineScope { - val results = downloadList.map { info -> - Log.d(TAG, "downloadSplitPackage: $info") - async { - runCatching { - httpClient.download(info.url, File(context.splitSaveFile().toString(), info.componentName), SPLIT_INSTALL_REQUEST_TAG) - }.onFailure { - Log.w(TAG, "downloadSplitPackage url:${info.url} save:${info.componentName}", it) - }.isSuccess.also { downloadSuccessful -> - splitInstallRecord[info] = if (downloadSuccessful) DownloadStatus.COMPLETE else DownloadStatus.FAILED - } - } - }.awaitAll() - return@coroutineScope results.all { it } - } - - @RequiresApi(Build.VERSION_CODES.M) - private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, packs: MutableSet): MutableList { - val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(callingPackage, 0)) - val requestUrl = - StringBuilder("$URL_DELIVERY?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=") - 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 components: MutableList = mutableListOf(); - splitPkgInfoList.filter { - !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() - }.forEach { info -> - packs.filter { - it.contains(info.splitPkgName!!) - }.forEach { - components.add(PackageComponent(callingPackage, it, info.downloadUrl!!)) - } - } - - Log.d(TAG, "requestDownloadUrls end -> $components") - components.forEach { - splitInstallRecord[it] = DownloadStatus.PENDING - } - - return components - } - + // TODO: use existing code private suspend fun getOauthToken(): String { val accounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE) var oauthToken: String? = null @@ -240,23 +267,40 @@ class SplitInstallManager(val context: Context) { return oauthToken ?: throw RuntimeException("oauthToken is null") } + /** + * Tests if a split apk has already been requested in this session. Returns true if it is + * pending or downloaded, and returns false if download failed or it is not yet known. + */ @RequiresApi(Build.VERSION_CODES.M) - private fun checkSplitInstalled(callingPackage: String, splitName: String): Boolean { + private fun shouldDownload(callingPackage: String, splitName: String): Boolean { return splitInstallRecord.keys.find { it.packageName == callingPackage && it.componentName == splitName } ?.let { - splitInstallRecord[it] != DownloadStatus.FAILED - } ?: false + splitInstallRecord[it] == DownloadStatus.FAILED + } ?: true } - internal fun notify(context: Context) { + /** + * Tell user about the ongoing download. + * TODO: make persistent + */ + internal fun notify(installForPackage: String) { 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) ) } + + val label = try { + context.packageManager.getPackageInfo(installForPackage, 0).applicationInfo + .loadLabel(context.packageManager) + } catch (e: NameNotFoundException) { + Log.e(TAG, "Couldn't load label for $installForPackage (${e.message}). Is it not installed?") + return + } + 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) + .setContentTitle(context.getString(R.string.split_install, label)).setPriority(NotificationCompat.PRIORITY_DEFAULT) .setDefaults( NotificationCompat.DEFAULT_ALL ).build().also { 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 c49356e723..59c8eae58e 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 @@ -46,7 +46,7 @@ class SplitInstallServiceImpl(private val installManager: SplitInstallManager, o override fun startInstall(pkg: String, splits: List, bundle0: Bundle, callback: ISplitInstallServiceCallback) { Log.d(TAG, "Method Called by package: $pkg") lifecycleScope.launch { - val installStatus = installManager.startInstall(pkg, splits) + val installStatus = installManager.splitInstallFlow(pkg, splits) Log.d(TAG, "startInstall: installStatus -> $installStatus") callback.onStartInstall(CommonStatusCodes.SUCCESS, Bundle()) } From 23d445977792d37f786f05ae16fc9401de849f62 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 21:37:59 +0200 Subject: [PATCH 37/59] `FLAG_MUTABLE` instead of `FLAG_IMMUTABLE` Otherwise response values cannot be written to our intent. --- .../android/finsky/splitinstallservice/SplitInstallManager.kt | 2 +- .../google/android/finsky/splitinstallservice/Uninstaller.kt | 2 +- 2 files changed, 2 insertions(+), 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 5fd591b683..6105cfa017 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 @@ -234,7 +234,7 @@ class SplitInstallManager(val context: Context) { putExtra(KEY_BYTES_DOWNLOADED, totalDownloaded) } val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) session.commit(pendingIntent.intentSender) Log.d(TAG, "installPackages session commit") return deferred.await() diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt index c4ca40647e..39384ec37e 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt @@ -18,7 +18,7 @@ fun Context.uninstallPackage(packageName: String) { installer.uninstall( packageName, PendingIntent.getBroadcast( this, session, Intent(this, InstallResultReceiver::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ).intentSender ) From e434c8cb4c90db9adb483a9c64f419aede62c4e8 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 21 Sep 2024 22:04:11 +0200 Subject: [PATCH 38/59] Refactor SplitInstallManager: structural pt. 2 --- .../org/microg/vending/ui/VendingActivity.kt | 2 +- .../android/vending/installer/Constants.kt | 16 ++++ .../com/android/vending/installer/Install.kt | 73 +++++++++++++++++++ .../vending/installer/Uninstall.kt} | 2 +- .../SplitInstallManager.kt | 71 ++---------------- 5 files changed, 98 insertions(+), 66 deletions(-) create mode 100644 vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt create mode 100644 vending-app/src/main/kotlin/com/android/vending/installer/Install.kt rename vending-app/src/main/kotlin/com/{google/android/finsky/splitinstallservice/Uninstaller.kt => android/vending/installer/Uninstall.kt} (94%) diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 7076e59fc3..6dd0647e83 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -31,7 +31,7 @@ import com.google.android.finsky.GoogleApiResponse import com.google.android.finsky.splitinstallservice.DownloadStatus import com.google.android.finsky.splitinstallservice.PackageComponent import com.google.android.finsky.splitinstallservice.SplitInstallManager -import com.google.android.finsky.splitinstallservice.uninstallPackage +import com.android.vending.installer.uninstallPackage import kotlinx.coroutines.runBlocking import org.microg.gms.common.DeviceConfiguration import org.microg.gms.common.asProto diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt new file mode 100644 index 0000000000..2a5dce36ce --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt @@ -0,0 +1,16 @@ +package com.android.vending.installer + +import android.content.Context +import android.content.Intent +import kotlinx.coroutines.CompletableDeferred +import java.io.File + +private const val FILE_SAVE_PATH = "phonesky-download-service" +internal const val TAG = "GmsPackageInstaller" + +const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" + + +fun Context.packageDownloadLocation() = File(filesDir, FILE_SAVE_PATH).apply { + if (!exists()) mkdir() +} diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt new file mode 100644 index 0000000000..de0de93cbb --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -0,0 +1,73 @@ +package com.android.vending.installer + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInfo +import android.content.pm.PackageInstaller +import android.content.pm.PackageInstaller.SessionParams +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver +import kotlinx.coroutines.CompletableDeferred +import java.io.File +import java.io.FileInputStream +import java.io.IOException + +@RequiresApi(Build.VERSION_CODES.M) +public suspend fun installPackages(context: Context, callingPackage: String, componentFiles: List, isUpdate: Boolean = false, deferredMap: MutableMap> = mutableMapOf()): Intent { + Log.v(TAG, "installPackages start") + + val packageInstaller = context.packageManager.packageInstaller + val installed = context.packageManager.getInstalledPackages(0).any { + it.applicationInfo.packageName == callingPackage + } + // Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed. + val params = SessionParams( + if (!installed || isUpdate) SessionParams.MODE_FULL_INSTALL + else SessionParams.MODE_INHERIT_EXISTING + ) + params.setAppPackageName(callingPackage) + params.setAppLabel(callingPackage + "Subcontracting") + params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) + try { + @SuppressLint("PrivateApi") val method = 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) + componentFiles.forEach { file -> + session.openWrite(file.name, 0, -1).use { outputStream -> + FileInputStream(file).use { inputStream -> inputStream.copyTo(outputStream) } + session.fsync(outputStream) + } + totalDownloaded += file.length() + file.delete() + } + val deferred = CompletableDeferred() + deferredMap[sessionId] = deferred + val intent = Intent(context, InstallResultReceiver::class.java).apply { + putExtra(KEY_BYTES_DOWNLOADED, totalDownloaded) + } + val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) + session.commit(pendingIntent.intentSender) + Log.d(TAG, "installPackages session commit") + return deferred.await() + } catch (e: IOException) { + Log.w(TAG, "Error installing packages", e) + throw e + } finally { + session?.close() + } +} diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt similarity index 94% rename from vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt rename to vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt index 39384ec37e..11d6befb59 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/Uninstaller.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt @@ -1,4 +1,4 @@ -package com.google.android.finsky.splitinstallservice +package com.android.vending.installer import android.app.PendingIntent import android.content.Context 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 6105cfa017..c2c595e586 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 @@ -30,7 +30,12 @@ import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.R import com.android.vending.buildRequestHeaders import com.android.vending.getAuthToken +import com.android.vending.installer.KEY_BYTES_DOWNLOADED +import com.android.vending.installer.installPackages +import com.android.vending.installer.packageDownloadLocation import com.google.android.finsky.GoogleApiResponse +import com.google.android.finsky.splitinstallservice.SplitInstallManager.Companion.deferredMap +import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -53,7 +58,6 @@ 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" @@ -62,7 +66,6 @@ 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 = "SplitInstallManager" class SplitInstallManager(val context: Context) { @@ -107,7 +110,6 @@ class SplitInstallManager(val context: Context) { } internal suspend fun downloadAndInstall(forPackage: String, downloadList: List, isUpdate: Boolean = false): Intent? { - if (!context.splitSaveFile().exists()) context.splitSaveFile().mkdir() val packageFiles = downloadPackageComponents(context, downloadList) val installFiles = packageFiles.map { if (it.value == null) { @@ -118,7 +120,7 @@ class SplitInstallManager(val context: Context) { Log.v(TAG, "splitInstallFlow downloaded success, downloaded ${installFiles.size} files") return runCatching { - installPackages(context, forPackage, installFiles, isUpdate) + installPackages(context, forPackage, installFiles, isUpdate, deferredMap) }.getOrNull() } @@ -172,7 +174,7 @@ class SplitInstallManager(val context: Context) { Log.d(TAG, "downloadSplitPackage: $info") async { info to runCatching { - val file = File(context.splitSaveFile().toString(), info.componentName) + val file = File(context.packageDownloadLocation().toString(), info.componentName) httpClient.download( url = info.url, downloadFile = file, @@ -188,64 +190,6 @@ class SplitInstallManager(val context: Context) { }.awaitAll().associate { it } } - @RequiresApi(Build.VERSION_CODES.M) - internal suspend fun installPackages(context: Context, callingPackage: String, componentFiles: List, isUpdate: Boolean = false): Intent { - Log.v(TAG, "installPackages start") - - val packageInstaller = context.packageManager.packageInstaller - val installed = context.packageManager.getInstalledPackages(0).any { - it.applicationInfo.packageName == callingPackage - } - // Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed. - val params = SessionParams( - if (!installed || isUpdate) SessionParams.MODE_FULL_INSTALL - else SessionParams.MODE_INHERIT_EXISTING - ) - params.setAppPackageName(callingPackage) - params.setAppLabel(callingPackage + "Subcontracting") - params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) - params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) - try { - @SuppressLint("PrivateApi") val method = 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) - componentFiles.forEach { file -> - val pkgPath = File(context.splitSaveFile(), file.name) - session.openWrite(file.name, 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, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) - session.commit(pendingIntent.intentSender) - Log.d(TAG, "installPackages session commit") - return deferred.await() - } catch (e: IOException) { - Log.w(TAG, "Error installing packages", e) - throw e - } finally { - session?.close() - } - } - // TODO: use existing code private suspend fun getOauthToken(): String { val accounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE) @@ -308,7 +252,6 @@ class SplitInstallManager(val context: Context) { } } - private fun Context.splitSaveFile() = File(filesDir, FILE_SAVE_PATH) private fun sendCompleteBroad(context: Context, packageName: String, intent: Intent) { Log.d(TAG, "sendCompleteBroadcast: intent:$intent") From 73f1a3aa81fa12a8810852707fcae4bbc8566344 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Sep 2024 12:14:33 +0200 Subject: [PATCH 39/59] Refactor SplitInstallManager: structural pt. 3 --- vending-app/src/main/AndroidManifest.xml | 4 +- .../org/microg/vending/billing/AuthManager.kt | 5 +- .../microg/vending/billing/core/HttpClient.kt | 2 +- .../vending/delivery/ComponentDownload.kt | 40 ++++ .../org/microg/vending/delivery/Delivery.kt | 79 +++++++ .../microg/vending/splitinstall/Constants.kt | 3 + .../org/microg/vending/ui/VendingActivity.kt | 67 +++--- .../kotlin/com/android/vending/extensions.kt | 2 +- .../android/vending/installer/Constants.kt | 3 - .../com/android/vending/installer/Install.kt | 27 ++- .../installer/SessionResultReceiver.kt | 60 +++++ .../android/vending/installer/Uninstall.kt | 22 +- .../splitinstallservice/PackageComponent.kt | 6 +- .../SplitInstallManager.kt | 215 ++++-------------- 14 files changed, 306 insertions(+), 229 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt create mode 100644 vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt create mode 100644 vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt create mode 100644 vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index d9de88954e..7b82f61d35 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -187,8 +187,8 @@ + android:name=".installer.SessionResultReceiver" + android:exported="false"/> + suspend fun download(url: String, downloadFile: File, tag: Any): String = suspendCoroutine { continuation -> val uriBuilder = Uri.parse(url).buildUpon() requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) { override fun parseNetworkResponse(response: NetworkResponse): Response { diff --git a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt new file mode 100644 index 0000000000..85aef50ee7 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt @@ -0,0 +1,40 @@ +package org.microg.vending.delivery + +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import com.android.vending.installer.packageDownloadLocation +import com.google.android.finsky.splitinstallservice.PackageComponent +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import org.microg.vending.billing.core.HttpClient +import java.io.File + +private const val TAG = "GmsVendingComponentDl" + +@RequiresApi(Build.VERSION_CODES.M) +suspend fun HttpClient.downloadPackageComponents( + context: Context, + downloadList: List, + tag: Any +): Map = coroutineScope { + downloadList.map { info -> + Log.d(TAG, "downloadSplitPackage: $info") + async { + info to runCatching { + val file = File(context.packageDownloadLocation().toString(), info.componentName) + download( + url = info.url, + downloadFile = file, + tag = tag + ) + file + }.onFailure { + Log.w(TAG, "package component failed to downlaod from url ${info.url}, " + + "to be saved as `${info.componentName}`", it) + }.getOrNull() + } + }.awaitAll().associate { it } +} diff --git a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt new file mode 100644 index 0000000000..bcb774edfc --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt @@ -0,0 +1,79 @@ +package org.microg.vending.delivery + +import android.util.Log +import com.android.vending.buildRequestHeaders +import com.google.android.finsky.GoogleApiResponse +import com.google.android.finsky.splitinstallservice.PackageComponent +import org.microg.vending.billing.core.AuthData +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY +import org.microg.vending.billing.core.HttpClient +import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG + +private const val TAG = "GmsVendingDelivery" + +/** + * Call the FDFE delivery endpoint to retrieve download URLs for the + * desired components. If specific split install packages are requested, + * only those will be contained in the result. + */ +suspend fun HttpClient.requestDownloadUrls( + packageName: String, + versionCode: Long, + auth: AuthData, + requestSplitPackages: List? = null, + deliveryToken: String? = null, +): List { + + val requestUrl = StringBuilder("$URL_DELIVERY?doc=$packageName&ot=1&vc=$versionCode") + + requestSplitPackages?.apply { + requestUrl.append( + "&bvc=$versionCode&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=" + ) + forEach { requestUrl.append("&mn=").append(it) } + } + + deliveryToken?.let { + requestUrl.append("&dtok=$it") + } + + Log.v(TAG, "requestDownloadUrls start") + val languages = requestSplitPackages?.filter { it.startsWith(SPLIT_LANGUAGE_TAG) }?.map { + it.replace(SPLIT_LANGUAGE_TAG, "") + } + Log.d(TAG, "requestDownloadUrls languages: $languages") + + val response = get( + url = requestUrl.toString(), + headers = buildRequestHeaders(auth.authToken, auth.gsfId.toLong(16), languages), + adapter = GoogleApiResponse.ADAPTER + ) + Log.d(TAG, "requestDownloadUrls end response -> $response") + + val basePackage = response.response!!.splitReqResult!!.pkgList?.let { + if (it.baseUrl != null && it.baseBytes != null) { + PackageComponent(packageName, "base", it.baseUrl, it.baseBytes) + } else null + } + val splitComponents = response.response.splitReqResult!!.pkgList!!.pkgDownLoadInfo.filter { + !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() + }.map { + if (requestSplitPackages != null) { + // Only download requested, if specific components were requested + requestSplitPackages.firstOrNull { requestComponent -> + requestComponent.contains(it.splitPkgName!!) + }?.let { requestComponent -> + PackageComponent(packageName, requestComponent, it.downloadUrl!!, it.size!!) + } + } else { + // Download all offered components (server chooses) + PackageComponent(packageName, it.splitPkgName!!, it.downloadUrl!!, it.size!!) + } + } + + val components = (listOf(basePackage) + splitComponents).filterNotNull() + + Log.d(TAG, "requestDownloadUrls end -> $components") + + return components +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt b/vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt new file mode 100644 index 0000000000..d4ab718438 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/splitinstall/Constants.kt @@ -0,0 +1,3 @@ +package org.microg.vending.splitinstall + +const val SPLIT_LANGUAGE_TAG = "config." diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 6dd0647e83..bd21b802a4 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -26,11 +26,9 @@ import com.android.vending.GetItemsResponse import com.android.vending.RequestApp import com.android.vending.RequestItem import com.android.vending.buildRequestHeaders +import com.android.vending.installer.installPackages import com.android.volley.VolleyError import com.google.android.finsky.GoogleApiResponse -import com.google.android.finsky.splitinstallservice.DownloadStatus -import com.google.android.finsky.splitinstallservice.PackageComponent -import com.google.android.finsky.splitinstallservice.SplitInstallManager import com.android.vending.installer.uninstallPackage import kotlinx.coroutines.runBlocking import org.microg.gms.common.DeviceConfiguration @@ -41,14 +39,15 @@ import org.microg.gms.ui.TAG import org.microg.vending.UploadDeviceConfigRequest import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.AuthData -import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_FDFE import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo +import org.microg.vending.delivery.downloadPackageComponents import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp +import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.ui.components.EnterpriseList import org.microg.vending.ui.components.NetworkState import java.io.IOException @@ -83,42 +82,50 @@ class VendingActivity : ComponentActivity() { Toast.makeText(this, "installing ${app.displayName} / ${app.packageName}", Toast.LENGTH_SHORT).show() Thread { runBlocking { + + val client = HttpClient(this@VendingActivity) + // Get download links for requested package - val res = HttpClient(this@VendingActivity).get( - url = URL_DELIVERY, - headers = buildRequestHeaders(auth!!.authToken, auth!!.gsfId.toLong(16)), - params = mapOf( - "ot" to "1", - "doc" to app.packageName, - "vc" to app.versionCode!!.toString() - ).plus(app.deliveryToken?.let { listOf("dtok" to it) } ?: emptyList()), - adapter = GoogleApiResponse.ADAPTER + val downloadUrls = client.requestDownloadUrls( + app.packageName, + app.versionCode!!.toLong(), + auth!!, + deliveryToken = app.deliveryToken ) - Log.d(TAG, res.toString()) - val components = listOf( - PackageComponent(app.packageName, "base", res.response!!.splitReqResult!!.pkgList!!.baseUrl!!) - ) + res.response.splitReqResult!!.pkgList!!.pkgDownLoadInfo.map { - PackageComponent(app.packageName, it.splitPkgName!!, it.downloadUrl!!) - } + val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls, Unit) + if (packageFiles.values.any { it == null }) { + Log.w(TAG, "Cannot proceed to installation as not all files were downloaded") + return@runBlocking + } - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - SplitInstallManager(this@VendingActivity).apply { - components.forEach { - SplitInstallManager.splitInstallRecord[it] = DownloadStatus.PENDING - } - notify(app.packageName) - downloadAndInstall(app.packageName, components, isUpdate) + val successfullyInstalled = runCatching { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + installPackages( + app.packageName, + packageFiles.values.filterNotNull(), + isUpdate + ) + } else { + TODO("implement installation on Lollipop installation") } - } else { - TODO("implement installation on Lollipop devices") + }.onSuccess { + load(account) } } }.start() } - val uninstall: (app: EnterpriseApp) -> Unit = { - uninstallPackage(it.packageName) + val uninstall: (app: EnterpriseApp) -> Unit = { app -> + Thread { + runBlocking { + + runCatching { uninstallPackage(app.packageName) }.onSuccess { + load(account) + } + + } + }.start() } setContent { diff --git a/vending-app/src/main/kotlin/com/android/vending/extensions.kt b/vending-app/src/main/kotlin/com/android/vending/extensions.kt index 12095facc2..b0c51f94ae 100644 --- a/vending-app/src/main/kotlin/com/android/vending/extensions.kt +++ b/vending-app/src/main/kotlin/com/android/vending/extensions.kt @@ -31,7 +31,7 @@ const val AUTH_TOKEN_SCOPE: String = "oauth2:https://www.googleapis.com/auth/goo private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING private const val FINSKY_VERSION = "Finsky/37.5.24-29%20%5B0%5D%20%5BPR%5D%20565477504" -fun buildRequestHeaders(auth: String, androidId: Long, language: List ?= null): Map { +fun buildRequestHeaders(auth: String, androidId: Long, language: List? = null): Map { var millis = System.currentTimeMillis() val timestamp = TimestampContainer.Builder().container2( TimestampContainer2.Builder().wrapper(TimestampWrapper.Builder().timestamp(makeTimestamp(millis)).build()).timestamp(makeTimestamp(millis)).build() diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt index 2a5dce36ce..93cd8aba1d 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt @@ -1,8 +1,6 @@ package com.android.vending.installer import android.content.Context -import android.content.Intent -import kotlinx.coroutines.CompletableDeferred import java.io.File private const val FILE_SAVE_PATH = "phonesky-download-service" @@ -10,7 +8,6 @@ internal const val TAG = "GmsPackageInstaller" const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" - fun Context.packageDownloadLocation() = File(filesDir, FILE_SAVE_PATH).apply { if (!exists()) mkdir() } diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index de0de93cbb..d25e886584 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -10,18 +10,21 @@ import android.content.pm.PackageInstaller.SessionParams import android.os.Build import android.util.Log import androidx.annotation.RequiresApi -import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver import kotlinx.coroutines.CompletableDeferred import java.io.File import java.io.FileInputStream import java.io.IOException @RequiresApi(Build.VERSION_CODES.M) -public suspend fun installPackages(context: Context, callingPackage: String, componentFiles: List, isUpdate: Boolean = false, deferredMap: MutableMap> = mutableMapOf()): Intent { +internal suspend fun Context.installPackages( + callingPackage: String, + componentFiles: List, + isUpdate: Boolean = false +) { Log.v(TAG, "installPackages start") - val packageInstaller = context.packageManager.packageInstaller - val installed = context.packageManager.getInstalledPackages(0).any { + val packageInstaller = packageManager.packageInstaller + val installed = packageManager.getInstalledPackages(0).any { it.applicationInfo.packageName == callingPackage } // Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed. @@ -54,14 +57,18 @@ public suspend fun installPackages(context: Context, callingPackage: String, com totalDownloaded += file.length() file.delete() } - val deferred = CompletableDeferred() - deferredMap[sessionId] = deferred - val intent = Intent(context, InstallResultReceiver::class.java).apply { - putExtra(KEY_BYTES_DOWNLOADED, totalDownloaded) - } - val pendingIntent = PendingIntent.getBroadcast(context, sessionId, intent, + val deferred = CompletableDeferred() + + SessionResultReceiver.pendingSessions[sessionId] = SessionResultReceiver.OnResult( + onSuccess = { deferred.complete(Unit) }, + onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) } + ) + + val intent = Intent(this, SessionResultReceiver::class.java) + val pendingIntent = PendingIntent.getBroadcast(this, sessionId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) session.commit(pendingIntent.intentSender) + Log.d(TAG, "installPackages session commit") return deferred.await() } catch (e: IOException) { diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt b/vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt new file mode 100644 index 0000000000..40ca932b2e --- /dev/null +++ b/vending-app/src/main/kotlin/com/android/vending/installer/SessionResultReceiver.kt @@ -0,0 +1,60 @@ +package com.android.vending.installer + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.pm.PackageInstaller +import android.os.Build +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import com.google.android.finsky.splitinstallservice.SplitInstallManager + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +internal class SessionResultReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) + val sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) + Log.d(TAG, "onReceive status: $status sessionId: $sessionId") + try { + when (status) { + PackageInstaller.STATUS_SUCCESS -> { + Log.d(TAG, "SessionResultReceiver received a successful transaction") + if (sessionId != -1) { + pendingSessions[sessionId]?.apply { onSuccess() } + pendingSessions.remove(sessionId) + } + } + + PackageInstaller.STATUS_PENDING_USER_ACTION -> { + val extraIntent = intent.extras?.getParcelable(Intent.EXTRA_INTENT) as Intent? + extraIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + extraIntent?.run { ContextCompat.startActivity(context, this, null) } + } + + else -> { + val errorMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) + Log.w(TAG, "SessionResultReceiver received a failed transaction result: $errorMessage") + if (sessionId != -1) { + pendingSessions[sessionId]?.apply { onFailure(errorMessage) } + pendingSessions.remove(sessionId) + } + } + } + } catch (e: Exception) { + Log.w(TAG, "SessionResultReceiver encountered error while handling session result", e) + if (sessionId != -1) { + pendingSessions[sessionId]?.apply { onFailure(e.message) } + } + } + } + + data class OnResult( + val onSuccess: () -> Unit, + val onFailure: (message: String?) -> Unit + ) + + companion object { + val pendingSessions: MutableMap = mutableMapOf() + } +} \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt index 11d6befb59..128cb3fe05 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Uninstall.kt @@ -5,21 +5,31 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller import androidx.annotation.RequiresApi -import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver - -class Uninstaller { -} +import kotlinx.coroutines.CompletableDeferred @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) -fun Context.uninstallPackage(packageName: String) { +suspend fun Context.uninstallPackage(packageName: String) { val installer = packageManager.packageInstaller val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) val session = installer.createSession(sessionParams) + + val deferred = CompletableDeferred() + + SessionResultReceiver.pendingSessions[session] = SessionResultReceiver.OnResult( + onSuccess = { deferred.complete(Unit) }, + onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) } + ) + installer.uninstall( packageName, PendingIntent.getBroadcast( - this, session, Intent(this, InstallResultReceiver::class.java), + this, session, Intent(this, SessionResultReceiver::class.java).apply { + // for an unknown reason, the session ID is not added to the response automatically :( + putExtra(PackageInstaller.EXTRA_SESSION_ID, session) + }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE ).intentSender ) + deferred.await() + } \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt index 3be462b41e..5d06d44fd5 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt @@ -3,5 +3,9 @@ package com.google.android.finsky.splitinstallservice data class PackageComponent( val packageName: String, val componentName: String, - val url: String + val url: String, + /** + * Size in bytes + */ + val size: Int ) \ No newline at end of file diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt index c2c595e586..d39146a5d4 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt @@ -4,19 +4,10 @@ */ package com.google.android.finsky.splitinstallservice -import android.accounts.Account -import android.accounts.AccountManager -import android.accounts.AuthenticatorException -import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager -import android.app.PendingIntent -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.pm.PackageInfo -import android.content.pm.PackageInstaller -import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import android.os.Bundle @@ -24,34 +15,21 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat import androidx.core.content.pm.PackageInfoCompat -import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.R -import com.android.vending.buildRequestHeaders -import com.android.vending.getAuthToken import com.android.vending.installer.KEY_BYTES_DOWNLOADED import com.android.vending.installer.installPackages -import com.android.vending.installer.packageDownloadLocation -import com.google.android.finsky.GoogleApiResponse -import com.google.android.finsky.splitinstallservice.SplitInstallManager.Companion.deferredMap -import com.google.android.finsky.splitinstallservice.SplitInstallManager.InstallResultReceiver import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext -import org.microg.vending.billing.DEFAULT_ACCOUNT_TYPE -import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY +import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.HttpClient -import java.io.File -import java.io.FileInputStream -import java.io.IOException +import org.microg.vending.delivery.downloadPackageComponents +import org.microg.vending.delivery.requestDownloadUrls +import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG private const val SPLIT_INSTALL_NOTIFY_ID = 111 private const val SPLIT_INSTALL_REQUEST_TAG = "splitInstallRequestTag" -private const val SPLIT_LANGUAGE_TAG = "config." private const val NOTIFY_CHANNEL_ID = "splitInstall" private const val NOTIFY_CHANNEL_NAME = "Split Install" @@ -86,31 +64,41 @@ class SplitInstallManager(val context: Context) { Log.v(TAG, "splitInstallFlow will query for these packages: $packagesToDownload") if (packagesToDownload.isEmpty()) return false - val oauthToken = runCatching { withContext(Dispatchers.IO) { - getOauthToken() + val authData = runCatching { withContext(Dispatchers.IO) { + AuthManager.getAuthData(context) } }.getOrNull() - Log.v(TAG, "splitInstallFlow oauthToken: $oauthToken") - if (oauthToken.isNullOrEmpty()) return false + Log.v(TAG, "splitInstallFlow oauthToken: $authData") + if (authData?.authToken.isNullOrEmpty()) return false + authData!! notify(callingPackage) - val components = runCatching { requestDownloadUrls(callingPackage, oauthToken, packagesToDownload) }.getOrNull() + val components = runCatching { + httpClient.requestDownloadUrls( + packageName = callingPackage, + versionCode = PackageInfoCompat.getLongVersionCode( + context.packageManager.getPackageInfo(callingPackage, 0) + ), + auth = authData, + requestSplitPackages = packagesToDownload + ) + }.getOrNull() Log.v(TAG, "splitInstallFlow requestDownloadUrls returned these components: $components") if (components.isNullOrEmpty()) { NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return false } - val intent = downloadAndInstall(callingPackage, components) + components.forEach { + splitInstallRecord[it] = DownloadStatus.PENDING + } - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) - if (intent == null) { return false } - sendCompleteBroad(context, callingPackage, intent) - return true - } + val packageFiles = httpClient.downloadPackageComponents(context, components, SPLIT_LANGUAGE_TAG) + + packageFiles.forEach { (component, downloadFile) -> + splitInstallRecord[component] = if (downloadFile != null) DownloadStatus.COMPLETE else DownloadStatus.FAILED + } - internal suspend fun downloadAndInstall(forPackage: String, downloadList: List, isUpdate: Boolean = false): Intent? { - val packageFiles = downloadPackageComponents(context, downloadList) val installFiles = packageFiles.map { if (it.value == null) { Log.w(TAG, "splitInstallFlow download failed, as ${it.key} was not downloaded") @@ -119,96 +107,17 @@ class SplitInstallManager(val context: Context) { } Log.v(TAG, "splitInstallFlow downloaded success, downloaded ${installFiles.size} files") - return runCatching { - installPackages(context, forPackage, installFiles, isUpdate, deferredMap) - }.getOrNull() - - } - - @RequiresApi(Build.VERSION_CODES.M) - private suspend fun requestDownloadUrls(callingPackage: String, authToken: String, requestSplitPackages: List): List { - val versionCode = PackageInfoCompat.getLongVersionCode(context.packageManager.getPackageInfo(callingPackage, 0)) - val requestUrl = - StringBuilder("$URL_DELIVERY?doc=$callingPackage&ot=1&vc=$versionCode&bvc=$versionCode" + - "&pf=1&pf=2&pf=3&pf=4&pf=5&pf=7&pf=8&pf=9&pf=10&da=4&bda=4&bf=4&fdcf=1&fdcf=2&ch=") - requestSplitPackages.forEach { requestUrl.append("&mn=").append(it) } - - Log.v(TAG, "requestDownloadUrls start") - val languages = requestSplitPackages.filter { it.startsWith(SPLIT_LANGUAGE_TAG) }.map { it.replace(SPLIT_LANGUAGE_TAG, "") } - Log.d(TAG, "requestDownloadUrls languages: $languages") - - val response = httpClient.get( - url = requestUrl.toString(), - headers = buildRequestHeaders(authToken, 1, languages).onEach { Log.d(TAG, "key:${it.key} value:${it.value}") }, - adapter = GoogleApiResponse.ADAPTER - ) - Log.d(TAG, "requestDownloadUrls end response -> $response") - - val splitPackageResponses = response.response!!.splitReqResult!!.pkgList!!.pkgDownLoadInfo.filter { - !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() - } - - val components: List = splitPackageResponses.mapNotNull { info -> - requestSplitPackages.firstOrNull { - it.contains(info.splitPkgName!!) - }?.let { - PackageComponent(callingPackage, it, info.downloadUrl!!) - } - } - - Log.d(TAG, "requestDownloadUrls end -> $components") - - components.forEach { - splitInstallRecord[it] = DownloadStatus.PENDING - } - - return components - } - - @RequiresApi(Build.VERSION_CODES.M) - private suspend fun downloadPackageComponents( - context: Context, - downloadList: List - ): Map = coroutineScope { - downloadList.map { info -> - Log.d(TAG, "downloadSplitPackage: $info") - async { - info to runCatching { - val file = File(context.packageDownloadLocation().toString(), info.componentName) - httpClient.download( - url = info.url, - downloadFile = file, - tag = SPLIT_INSTALL_REQUEST_TAG - ) - file - }.onFailure { - Log.w(TAG, "downloadSplitPackage failed to downlaod from url:${info.url} to be saved as `${info.componentName}`", it) - }.also { - splitInstallRecord[info] = if (it.isSuccess) DownloadStatus.COMPLETE else DownloadStatus.FAILED - }.getOrNull() - } - }.awaitAll().associate { it } - } + val success = runCatching { + context.installPackages(callingPackage, installFiles, false) + }.isSuccess - // TODO: use existing code - private suspend fun getOauthToken(): String { - val accounts = AccountManager.get(context).getAccountsByType(DEFAULT_ACCOUNT_TYPE) - var oauthToken: String? = null - if (accounts.isEmpty()) { - Log.w(TAG, "No Google account found") - throw RuntimeException("No Google account found") - } else for (account: Account in accounts) { - oauthToken = try { - getAuthToken(AccountManager.get(context), account, AUTH_TOKEN_SCOPE).getString(AccountManager.KEY_AUTHTOKEN) - } catch (e: AuthenticatorException) { - Log.w(TAG, "Could not fetch auth token for account $account") - null - } - if (oauthToken != null) { - break - } + NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) + return if (success) { + sendCompleteBroad(context, callingPackage, components.sumOf { it.size.toLong() }) + true + } else { + false } - return oauthToken ?: throw RuntimeException("oauthToken is null") } /** @@ -252,16 +161,15 @@ class SplitInstallManager(val context: Context) { } } - - private fun sendCompleteBroad(context: Context, packageName: String, intent: Intent) { - Log.d(TAG, "sendCompleteBroadcast: intent:$intent") + private fun sendCompleteBroad(context: Context, packageName: String, bytes: Long) { + Log.d(TAG, "sendCompleteBroadcast: $bytes bytes") val extra = Bundle().apply { putInt(KEY_STATUS, 5) putInt(KEY_ERROR_CODE, 0) putInt(KEY_SESSION_ID, 0) - putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0)) - putString(KEY_LANGUAGES, intent.getStringExtra(KEY_LANGUAGE)) - putLong(KEY_BYTES_DOWNLOADED, intent.getLongExtra(KEY_BYTES_DOWNLOADED, 0)) + putLong(KEY_TOTAL_BYTES_TO_DOWNLOAD, bytes) + //putString(KEY_LANGUAGES, intent.getStringExtra(KEY_LANGUAGE)) + putLong(KEY_BYTES_DOWNLOADED, bytes) } val broadcastIntent = Intent(ACTION_UPDATE_SERVICE).apply { setPackage(packageName) @@ -278,47 +186,6 @@ class SplitInstallManager(val context: Context) { deferredMap.clear() } - internal class InstallResultReceiver : BroadcastReceiver() { - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - override fun onReceive(context: Context, intent: Intent) { - val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) - val sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) - Log.d(TAG, "onReceive status: $status sessionId: $sessionId") - try { - when (status) { - PackageInstaller.STATUS_SUCCESS -> { - Log.d(TAG, "InstallResultReceiver onReceive: install success") - if (sessionId != -1) { - deferredMap[sessionId]?.complete(intent) - deferredMap.remove(sessionId) - } - } - - PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val extraIntent = intent.extras?.getParcelable(Intent.EXTRA_INTENT) as Intent? - extraIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - extraIntent?.run { ContextCompat.startActivity(context, this, null) } - } - - else -> { - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) - val errorMsg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) - Log.w(TAG, "InstallResultReceiver onReceive: install fail -> $errorMsg") - if (sessionId != -1) { - deferredMap[sessionId]?.completeExceptionally(RuntimeException("install fail -> $errorMsg")) - deferredMap.remove(sessionId) - } - } - } - } catch (e: Exception) { - Log.w(TAG, "Error handling install result", e) - if (sessionId != -1) { - deferredMap[sessionId]?.completeExceptionally(e) - } - } - } - } - companion object { // Installation records, including (sub)package name, download path, and installation status internal val splitInstallRecord: MutableMap = mutableMapOf() From 3b8509de9e0a1e6a63034fd37d61e2d00a630338 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Sep 2024 13:06:45 +0200 Subject: [PATCH 40/59] Work vending: Show installation success without reload --- .../vending/delivery/ComponentDownload.kt | 2 +- .../java/org/microg/vending/enterprise/App.kt | 23 +------ .../org/microg/vending/enterprise/AppState.kt | 24 +++++++ .../vending/enterprise/EnterpriseApp.kt | 3 +- .../org/microg/vending/ui/VendingActivity.kt | 67 +++++++++++-------- .../microg/vending/ui/components/AppRow.kt | 35 +++++++--- .../vending/ui/components/EnterpriseList.kt | 24 ++++--- .../com/android/vending/installer/Install.kt | 2 +- 8 files changed, 106 insertions(+), 74 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt diff --git a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt index 85aef50ee7..e6a3957f68 100644 --- a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt +++ b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt @@ -14,7 +14,7 @@ import java.io.File private const val TAG = "GmsVendingComponentDl" -@RequiresApi(Build.VERSION_CODES.M) +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) suspend fun HttpClient.downloadPackageComponents( context: Context, downloadList: List, diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt index 98d0d9e20c..59c5b0bc08 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/App.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/App.kt @@ -4,27 +4,6 @@ open class App( val packageName: String, val versionCode: Int?, val displayName: String, - val state: State, val iconUrl: String?, val deliveryToken: String? -) { - enum class State { - /** - * App cannot be installed on this user's device - */ - NOT_COMPATIBLE, - /** - * App is available, but not installed on the user's device. - */ - NOT_INSTALLED, - /** - * App is already installed on the device, but an update is available. - */ - UPDATE_AVAILABLE, - - /** - * App is installed on device and up to date. - */ - INSTALLED - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt b/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt new file mode 100644 index 0000000000..339f82f3dd --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt @@ -0,0 +1,24 @@ +package org.microg.vending.enterprise + +enum class AppState { + /** + * App cannot be installed on this user's device + */ + NOT_COMPATIBLE, + /** + * App is available, but not installed on the user's device. + */ + NOT_INSTALLED, + /** + * App is already installed on the device, but an update is available. + */ + UPDATE_AVAILABLE, + /** + * An app operation is currently outstanding + */ + PENDING, + /** + * App is installed on device and up to date. + */ + INSTALLED +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt index 5dedc7b2ab..a80864b590 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt @@ -6,8 +6,7 @@ class EnterpriseApp( packageName: String, versionCode: Int?, displayName: String, - state: State, iconUrl: String?, deliveryToken: String?, val policy: AppInstallPolicy -) : App(packageName, versionCode, displayName, state, iconUrl, deliveryToken) \ No newline at end of file +) : App(packageName, versionCode, displayName, iconUrl, deliveryToken) \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index bd21b802a4..9d7658b96a 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -4,7 +4,6 @@ import android.accounts.Account import android.accounts.AccountManager import android.os.Bundle import android.util.Log -import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -17,6 +16,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -48,6 +48,7 @@ import org.microg.vending.delivery.downloadPackageComponents import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp import org.microg.vending.delivery.requestDownloadUrls +import org.microg.vending.enterprise.AppState import org.microg.vending.ui.components.EnterpriseList import org.microg.vending.ui.components.NetworkState import java.io.IOException @@ -55,7 +56,7 @@ import java.io.IOException @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) class VendingActivity : ComponentActivity() { - var apps: MutableList = mutableStateListOf() + var apps: MutableMap = mutableStateMapOf() var networkState by mutableStateOf(NetworkState.ACTIVE) var auth: AuthData? = null @@ -79,38 +80,45 @@ class VendingActivity : ComponentActivity() { load(account) val install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit = { app, isUpdate -> - Toast.makeText(this, "installing ${app.displayName} / ${app.packageName}", Toast.LENGTH_SHORT).show() Thread { runBlocking { + val previousState = apps[app]!! + apps[app] = AppState.PENDING + val client = HttpClient(this@VendingActivity) // Get download links for requested package - val downloadUrls = client.requestDownloadUrls( + val downloadUrls = runCatching { client.requestDownloadUrls( app.packageName, app.versionCode!!.toLong(), auth!!, deliveryToken = app.deliveryToken - ) + ) } + + if (downloadUrls.isFailure) { + Log.w(TAG, "Failed to request download URLs: ${downloadUrls.exceptionOrNull()!!.message}") + apps[app] = previousState + return@runBlocking + } - val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls, Unit) + val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls.getOrThrow(), Unit) if (packageFiles.values.any { it == null }) { Log.w(TAG, "Cannot proceed to installation as not all files were downloaded") + apps[app] = previousState return@runBlocking } - val successfullyInstalled = runCatching { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - installPackages( - app.packageName, - packageFiles.values.filterNotNull(), - isUpdate - ) - } else { - TODO("implement installation on Lollipop installation") - } + runCatching { + installPackages( + app.packageName, + packageFiles.values.filterNotNull(), + isUpdate + ) }.onSuccess { - load(account) + apps[app] = AppState.INSTALLED + }.onFailure { + apps[app] = previousState } } }.start() @@ -120,8 +128,12 @@ class VendingActivity : ComponentActivity() { Thread { runBlocking { + val previousState = apps[app]!! + apps[app] = AppState.PENDING runCatching { uninstallPackage(app.packageName) }.onSuccess { - load(account) + apps[app] = AppState.NOT_INSTALLED + }.onFailure { + apps[app] = previousState } } @@ -190,7 +202,7 @@ class VendingActivity : ComponentActivity() { RequestItem(RequestApp(AppMeta(it.packageName))) } ) - ).items.map { it.response }.filterNotNull().map { item -> + ).items.map { it.response }.filterNotNull().associate { item -> val packageName = item.meta!!.packageName!! val installedDetails = this@VendingActivity.packageManager.getInstalledPackages(0).find { it.applicationInfo.packageName == packageName @@ -202,28 +214,27 @@ class VendingActivity : ComponentActivity() { item.offer!!.version!!.versionCode!! } else null - val state = if (!available && installedDetails == null) App.State.NOT_COMPATIBLE - else if (!available && installedDetails != null) App.State.INSTALLED - else if (available && installedDetails == null) App.State.NOT_INSTALLED - else if (available && installedDetails != null && installedDetails.versionCode > versionCode!!) App.State.UPDATE_AVAILABLE - else /* if (available && installedDetails != null) */ App.State.INSTALLED + val state = if (!available && installedDetails == null) AppState.NOT_COMPATIBLE + else if (!available && installedDetails != null) AppState.INSTALLED + else if (available && installedDetails == null) AppState.NOT_INSTALLED + else if (available && installedDetails != null && installedDetails.versionCode > versionCode!!) AppState.UPDATE_AVAILABLE + else /* if (available && installedDetails != null) */ AppState.INSTALLED EnterpriseApp( packageName, versionCode, item.detail!!.name!!.displayName!!, - state, item.detail.icon?.icon?.paint?.url, item.offer?.delivery?.key, apps.find { it.packageName!! == item.meta.packageName }!!.policy!!, - ) + ) to state }.onEach { - Log.v(TAG, "${it.packageName} delivery token: ${it.deliveryToken ?: "none acquired"}") + Log.v(TAG, "${it.key.packageName} (state: ${it.value}) delivery token: ${it.key.deliveryToken ?: "none acquired"}") } this@VendingActivity.apps.apply { clear() - addAll(details) + putAll(details) } networkState = NetworkState.PASSIVE } catch (e: IOException) { diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt index 03ec321a22..b49c36e05b 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -24,9 +25,10 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.android.vending.R import org.microg.vending.enterprise.App +import org.microg.vending.enterprise.AppState @Composable -fun AppRow(app: App, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) { +fun AppRow(app: App, state: AppState, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) { Row( Modifier.padding(top = 8.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), @@ -45,48 +47,59 @@ fun AppRow(app: App, install: () -> Unit, update: () -> Unit, uninstall: () -> U Text(app.displayName) Spacer(Modifier.weight(1f)) - if (app.state == App.State.NOT_COMPATIBLE) { + if (state == AppState.NOT_COMPATIBLE) { Icon(Icons.Default.Warning, null, Modifier.padding(end=8.dp), tint = MaterialTheme.colorScheme.secondary) // TODO better UI } - if (app.state == App.State.UPDATE_AVAILABLE || app.state == App.State.INSTALLED) { + if (state == AppState.UPDATE_AVAILABLE || state == AppState.INSTALLED) { IconButton(uninstall) { Icon(Icons.Default.Delete, stringResource(R.string.vending_overview_row_action_uninstall), tint = MaterialTheme.colorScheme.secondary) } } - if (app.state == App.State.UPDATE_AVAILABLE) { + if (state == AppState.UPDATE_AVAILABLE) { FilledIconButton(update, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { Icon(painterResource(R.drawable.ic_update), stringResource(R.string.vending_overview_row_action_update), tint = MaterialTheme.colorScheme.secondary) } } - if (app.state == App.State.NOT_INSTALLED) - FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { - Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary) + if (state == AppState.NOT_INSTALLED) { + FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { + Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary) + } + } + if (state == AppState.PENDING) { + CircularProgressIndicator(Modifier.padding(4.dp)) } } + } +private val previewApp = App("org.mozilla.firefox", 0, "Firefox", null, null) @Preview @Composable fun AppRowNotCompatiblePreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_COMPATIBLE, null, null), {}, {}, {}) + AppRow(previewApp, AppState.NOT_COMPATIBLE, {}, {}, {}) } @Preview @Composable fun AppRowNotInstalledPreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, ""), {}, {}, {}) + AppRow(previewApp, AppState.NOT_INSTALLED, {}, {}, {}) } @Preview @Composable fun AppRowUpdateablePreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.UPDATE_AVAILABLE, null, ""), {}, {}, {}) + AppRow(previewApp, AppState.UPDATE_AVAILABLE, {}, {}, {}) } @Preview @Composable fun AppRowInstalledPreview() { - AppRow(App("org.mozilla.firefox", 0, "Firefox", App.State.INSTALLED, null, ""), {}, {}, {}) + AppRow(previewApp, AppState.INSTALLED, {}, {}, {}) } +@Preview +@Composable +fun AppRowPendingPreview() { + AppRow(previewApp, AppState.PENDING, {}, {}, {}) +} diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index 62448300af..900e08815e 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -26,24 +26,30 @@ import androidx.compose.ui.unit.dp import com.android.vending.R import com.google.android.finsky.AppInstallPolicy import org.microg.vending.enterprise.App +import org.microg.vending.enterprise.AppState import org.microg.vending.enterprise.EnterpriseApp @Composable -fun EnterpriseList(apps: List, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { - if (apps.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) { +fun EnterpriseList(appStates: Map, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { + if (appStates.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) { + val apps = appStates.keys val requiredApps = apps.filter { it.policy == AppInstallPolicy.MANDATORY } if (requiredApps.isNotEmpty()) { item { InListHeading(R.string.vending_overview_enterprise_row_mandatory) } item { InListWarning(R.string.vending_overview_enterprise_row_mandatory_hint) } - items(requiredApps) { AppRow(it, { install(it, false) }, { install(it, true) }, { uninstall(it) }) } + items(requiredApps.sortedBy { it.packageName }) { + AppRow(it, appStates[it]!!, { install(it, false) }, { install(it, true) }, { uninstall(it) }) + } } val optionalApps = apps.filter { it.policy == AppInstallPolicy.OPTIONAL } if (optionalApps.isNotEmpty()) { item { InListHeading(R.string.vending_overview_enterprise_row_offered) } - items(optionalApps) { AppRow(it, { install(it, false) }, { install(it, true) }, { uninstall(it) }) } + items(optionalApps.sortedBy { it.packageName }) { + AppRow(it, appStates[it]!!, { install(it, false) }, { install(it, true) }, { uninstall(it) }) + } } } else Box( @@ -99,10 +105,10 @@ fun InListWarning(@StringRes text: Int) { @Composable fun EnterpriseListPreview() { EnterpriseList( - listOf( - EnterpriseApp("com.android.vending", 0, "Market", App.State.INSTALLED, null, "", AppInstallPolicy.MANDATORY), - EnterpriseApp("org.mozilla.firefox", 0, "Firefox", App.State.NOT_INSTALLED, null, "", AppInstallPolicy.OPTIONAL), - EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", App.State.NOT_COMPATIBLE, null, "", AppInstallPolicy.OPTIONAL) + mapOf( + EnterpriseApp("com.android.vending", 0, "Market", null, "", AppInstallPolicy.MANDATORY) to AppState.INSTALLED, + EnterpriseApp("org.mozilla.firefox", 0, "Firefox", null, "", AppInstallPolicy.OPTIONAL) to AppState.NOT_INSTALLED, + EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", null, "", AppInstallPolicy.OPTIONAL) to AppState.NOT_COMPATIBLE ), { _, _ -> }, {} ) } @@ -110,5 +116,5 @@ fun EnterpriseListPreview() { @Preview @Composable fun EnterpriseListEmptyPreview() { - EnterpriseList(emptyList(), { _, _ -> }, {}) + EnterpriseList(emptyMap(), { _, _ -> }, {}) } diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index d25e886584..f61a09f90c 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -15,7 +15,7 @@ import java.io.File import java.io.FileInputStream import java.io.IOException -@RequiresApi(Build.VERSION_CODES.M) +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) internal suspend fun Context.installPackages( callingPackage: String, componentFiles: List, From 68ef487299ccca450095733fb0015a7a831fa5fa Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Sep 2024 13:17:37 +0200 Subject: [PATCH 41/59] Use cache dir instead of files dir Installed packages are deleted after installation, but if installation fails and never retried (for instance: one large package is downloaded successfully, then a small package is downloaded unsuccessfully due to a network error, then the user uninstalls the app because it doesn't work), this can still lead to storage leaks from downloaded-but-never-installed apps. We should use `cacheDir` instead: it will allow the system and users to clear the cache themselves. --- .../src/main/kotlin/com/android/vending/installer/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt index 93cd8aba1d..9681798f91 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Constants.kt @@ -8,6 +8,6 @@ internal const val TAG = "GmsPackageInstaller" const val KEY_BYTES_DOWNLOADED = "bytes_downloaded" -fun Context.packageDownloadLocation() = File(filesDir, FILE_SAVE_PATH).apply { +fun Context.packageDownloadLocation() = File(cacheDir, FILE_SAVE_PATH).apply { if (!exists()) mkdir() } From f257e8362aafb6681d532440f52f1e659cfa428d Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Sep 2024 14:31:24 +0200 Subject: [PATCH 42/59] Enable work app store only after adding a work account --- .../authenticator/WorkAccountAuthenticator.kt | 10 ++++++ .../auth/workaccount/WorkAccountService.kt | 16 +++++++-- vending-app/src/main/AndroidManifest.xml | 11 +++++- .../vending/WorkAccountChangedReceiver.kt | 34 +++++++++++++++++++ .../org/microg/vending/ui/VendingActivity.kt | 10 ++++-- 5 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 73cce5bc21..4708fb348d 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -10,6 +10,7 @@ import android.accounts.Account import android.accounts.AccountAuthenticatorResponse import android.accounts.AccountManager import android.content.Context +import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log @@ -80,6 +81,12 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica putString(KEY_ACCOUNT_SERVICES, authResponse.services) // expected to be "android" } )) { + + // Notify vending package + context.sendBroadcast( + Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending") + ) + // Report successful creation to caller response.onResult(Bundle().apply { putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email) @@ -203,6 +210,9 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica companion object { const val TAG = "WorkAccAuthenticator" const val WORK_ACCOUNT_TYPE = "com.google.work" + + const val WORK_ACCOUNT_CHANGED_BOARDCAST = "org.microg.vending.WORK_ACCOUNT_CHANGED" + const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken" private const val KEY_GOOGLE_USER_ID = "GoogleUserId" // TODO: use AuthConstants private const val KEY_ACCOUNT_SERVICES = "services" // TODO: use AuthConstants diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 3f6145dce3..488a872631 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -10,6 +10,7 @@ import android.accounts.AccountManager import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context +import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.os.Bundle @@ -18,7 +19,9 @@ import android.util.Log import com.google.android.gms.auth.account.IWorkAccountCallback import com.google.android.gms.auth.account.IWorkAccountService import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.KEY_ACCOUNT_CREATION_TOKEN +import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_CHANGED_BOARDCAST import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_TYPE +import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes import com.google.android.gms.common.internal.ConnectionInfo @@ -68,7 +71,7 @@ private fun DevicePolicyManager.isDeviceAdminApp(packageName: String?): Boolean } } -class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { +class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() { val packageManager: PackageManager = context.packageManager val accountManager: AccountManager = AccountManager.get(context) @@ -82,8 +85,8 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { Log.d(TAG, "setWorkAuthenticatorEnabled with $enabled") val componentName = ComponentName( - "com.google.android.gms", - "com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService" + context, + WorkAccountAuthenticatorService::class.java ) packageManager.setComponentEnabledSetting( componentName, @@ -127,7 +130,14 @@ class WorkAccountServiceImpl(context: Context) : IWorkAccountService.Stub() { Log.d(TAG, "removeWorkAccount with account ${account?.name}") account?.let { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + val success = accountManager.removeAccountExplicitly(it) + + // Notify vending package + context.sendBroadcast( + Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending") + ) + callback?.onAccountRemoved(success) } else { val future = accountManager.removeAccount(it, null, null) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 7b82f61d35..073caa0bd2 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -189,16 +189,25 @@ + + + android:label="@string/vending_activity_name" + android:enabled="false"> + + + + + diff --git a/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt b/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt new file mode 100644 index 0000000000..803ce48366 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt @@ -0,0 +1,34 @@ +package org.microg.vending + +import android.accounts.AccountManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.util.Log +import org.microg.vending.ui.VendingActivity + +class WorkAccountChangedReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent?) { + val accountManager = AccountManager.get(context) + val hasWorkAccounts = accountManager.getAccountsByType("com.google.work").isNotEmpty() + + Log.d(TAG, "setting VendingActivity state to enabled = $hasWorkAccounts") + + val componentName = ComponentName( + context, + VendingActivity::class.java + ) + context.packageManager.setComponentEnabledSetting( + componentName, + if (hasWorkAccounts) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + 0 + ) + } + + companion object { + const val TAG = "GmsVendingWorkAccRcvr" + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 9d7658b96a..51202c7b38 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -2,6 +2,7 @@ package org.microg.vending.ui import android.accounts.Account import android.accounts.AccountManager +import android.content.Intent import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity @@ -15,7 +16,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -37,6 +37,7 @@ import org.microg.gms.profile.Build import org.microg.gms.profile.ProfileManager import org.microg.gms.ui.TAG import org.microg.vending.UploadDeviceConfigRequest +import org.microg.vending.WorkAccountChangedReceiver import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.AuthData import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY @@ -45,7 +46,6 @@ import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo import org.microg.vending.delivery.downloadPackageComponents -import org.microg.vending.enterprise.App import org.microg.vending.enterprise.EnterpriseApp import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.enterprise.AppState @@ -70,6 +70,10 @@ class VendingActivity : ComponentActivity() { val accountManager = AccountManager.get(this) val accounts = accountManager.getAccountsByType("com.google.work") if (accounts.isEmpty()) { + // Component should not be enabled; disable through receiver, and redirect to main activity + WorkAccountChangedReceiver().onReceive(this, null) + startActivity(Intent(this, MainActivity::class.java)) + finish() TODO("App should only be visible if work accounts are added. Disable component and wonder why it was enabled in the first place") } else if (accounts.size > 1) { Log.w(TAG, "Multiple work accounts found. This is unexpected and could point " + @@ -217,7 +221,7 @@ class VendingActivity : ComponentActivity() { val state = if (!available && installedDetails == null) AppState.NOT_COMPATIBLE else if (!available && installedDetails != null) AppState.INSTALLED else if (available && installedDetails == null) AppState.NOT_INSTALLED - else if (available && installedDetails != null && installedDetails.versionCode > versionCode!!) AppState.UPDATE_AVAILABLE + else if (available && installedDetails != null && installedDetails.versionCode < versionCode!!) AppState.UPDATE_AVAILABLE else /* if (available && installedDetails != null) */ AppState.INSTALLED EnterpriseApp( From 1887759b448c104d5383a045e6cacbad0682548d Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Sep 2024 14:41:02 +0200 Subject: [PATCH 43/59] Fix lint --- vending-app/src/main/AndroidManifest.xml | 7 ++++-- .../vending/WorkAccountChangedReceiver.kt | 24 +++++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 073caa0bd2..16a64c792c 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -20,10 +20,13 @@ - - + + + diff --git a/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt b/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt index 803ce48366..32af2f730b 100644 --- a/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt +++ b/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt @@ -6,6 +6,7 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.os.Build import android.util.Log import org.microg.vending.ui.VendingActivity @@ -15,17 +16,20 @@ class WorkAccountChangedReceiver : BroadcastReceiver() { val accountManager = AccountManager.get(context) val hasWorkAccounts = accountManager.getAccountsByType("com.google.work").isNotEmpty() - Log.d(TAG, "setting VendingActivity state to enabled = $hasWorkAccounts") - val componentName = ComponentName( - context, - VendingActivity::class.java - ) - context.packageManager.setComponentEnabledSetting( - componentName, - if (hasWorkAccounts) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, - 0 - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + Log.d(TAG, "setting VendingActivity state to enabled = $hasWorkAccounts") + + val componentName = ComponentName( + context, + VendingActivity::class.java + ) + context.packageManager.setComponentEnabledSetting( + componentName, + if (hasWorkAccounts) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + 0 + ) + } } companion object { From 85bd1c9272392f968986b5a52d058a1841ea7c59 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sun, 22 Sep 2024 18:07:22 +0200 Subject: [PATCH 44/59] Fix split installs --- .../org/microg/vending/delivery/Delivery.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt index bcb774edfc..cffc1d82d0 100644 --- a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt +++ b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt @@ -43,9 +43,19 @@ suspend fun HttpClient.requestDownloadUrls( } Log.d(TAG, "requestDownloadUrls languages: $languages") + val headers = buildRequestHeaders( + auth = auth.authToken, + // TODO: understand behavior. Using proper Android ID doesn't work when downloading split APKs + androidId = if (requestSplitPackages != null) 1 else auth.gsfId.toLong(16), + languages + ).minus( + // TODO: understand behavior. According to tests, these headers break split install queries but may be needed for normal ones + (if (requestSplitPackages != null) listOf("X-DFE-Encoded-Targets", "X-DFE-Phenotype", "X-DFE-Device-Id", "X-DFE-Client-Id") else emptyList()).toSet() + ) + val response = get( url = requestUrl.toString(), - headers = buildRequestHeaders(auth.authToken, auth.gsfId.toLong(16), languages), + headers = headers, adapter = GoogleApiResponse.ADAPTER ) Log.d(TAG, "requestDownloadUrls end response -> $response") @@ -71,7 +81,11 @@ suspend fun HttpClient.requestDownloadUrls( } } - val components = (listOf(basePackage) + splitComponents).filterNotNull() + val components = if (requestSplitPackages != null) { + splitComponents + } else { + listOf(basePackage) + splitComponents + }.filterNotNull() Log.d(TAG, "requestDownloadUrls end -> $components") From 2e1dfece008bad1d08614410167621b8aa7c3059 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 25 Sep 2024 12:15:10 +0200 Subject: [PATCH 45/59] Organize vending protobuf files --- .../vending/licensing/LicenseChecker.kt | 10 +- .../vending/billing/AcquireFreeAppLicense.kt | 8 +- .../microg/vending/billing/core/IAPCore.kt | 14 +-- .../vending/delivery/ComponentDownload.kt | 2 +- .../org/microg/vending/delivery/Delivery.kt | 12 +-- .../vending/enterprise/EnterpriseApp.kt | 2 +- .../org/microg/vending/ui/VendingActivity.kt | 21 ++-- .../vending/ui/components/EnterpriseList.kt | 3 +- .../kotlin/com/android/vending/extensions.kt | 21 +++- .../src/main/proto/DeliveryResponse.proto | 58 +++++++++++ .../main/proto/EnterpriseClientPolicy.proto | 4 +- .../src/main/proto/GetItemsRequest.proto | 2 +- .../src/main/proto/GetItemsResponse.proto | 11 +-- .../src/main/proto/GooglePlayResponse.proto | 60 ++++++++++++ .../src/main/proto/LicenseResult.proto | 15 +-- vending-app/src/main/proto/Locality.proto | 2 +- .../{GooglePlay.proto => Purchase.proto} | 38 +------- .../src/main/proto/RequestHeader.proto | 2 +- vending-app/src/main/proto/SplitInstall.proto | 96 ------------------- vending-app/src/main/proto/Timestamp.proto | 2 +- .../proto/UploadDeviceConfigResponse.proto | 7 ++ 21 files changed, 193 insertions(+), 197 deletions(-) create mode 100644 vending-app/src/main/proto/DeliveryResponse.proto create mode 100644 vending-app/src/main/proto/GooglePlayResponse.proto rename vending-app/src/main/proto/{GooglePlay.proto => Purchase.proto} (93%) delete mode 100644 vending-app/src/main/proto/SplitInstall.proto create mode 100644 vending-app/src/main/proto/UploadDeviceConfigResponse.proto 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 5567c39395..c180e805a2 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 @@ -8,11 +8,11 @@ import android.content.pm.PackageInfo 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 org.microg.vending.billing.proto.GoogleApiResponse import java.io.IOException private const val TAG = "FakeLicenseChecker" @@ -142,8 +142,8 @@ suspend fun HttpClient.makeLicenseV1Request( ): V1Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicense?pkgn=$packageName&vc=$versionCode&nnc=$nonce", headers = buildRequestHeaders(auth, androidId), - adapter = LicenseResult.ADAPTER -).information?.v1?.let { + adapter = GoogleApiResponse.ADAPTER +).payload?.licenseV1Response?.let { if (it.result != null && it.signedData != null && it.signature != null) { V1Response(it.result, it.signedData, it.signature) } else null @@ -157,8 +157,8 @@ suspend fun HttpClient.makeLicenseV2Request( ): V2Response? = get( url = "https://play-fe.googleapis.com/fdfe/apps/checkLicenseServerFallback?pkgn=$packageName&vc=$versionCode", headers = buildRequestHeaders(auth, androidId), - adapter = LicenseResult.ADAPTER -).information?.v2?.license?.jwt?.let { + adapter = GoogleApiResponse.ADAPTER +).payload?.licenseV2Response?.license?.jwt?.let { // Field present ←→ user has license V2Response(LICENSED, it) } \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt b/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt index 56f3d128eb..0f28237f9a 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt @@ -8,7 +8,7 @@ import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_PURCHASE import org.microg.vending.billing.core.HeaderProvider import org.microg.vending.billing.core.HttpClient -import org.microg.vending.billing.proto.ResponseWrapper +import org.microg.vending.billing.proto.GoogleApiResponse suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, packageName: String): Boolean { val authData = AuthManager.getAuthData(context, account) @@ -27,7 +27,7 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, url = URL_DETAILS, headers = headers, params = mapOf("doc" to packageName), - adapter = ResponseWrapper.ADAPTER + adapter = GoogleApiResponse.ADAPTER ).payload?.detailsResponse } catch (e: VolleyError) { Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response when gathering app data") @@ -65,14 +65,14 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, url = URL_PURCHASE, headers = headers, params = parameters, - adapter = ResponseWrapper.ADAPTER + adapter = GoogleApiResponse.ADAPTER ).payload?.buyResponse } catch (e: VolleyError) { Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase") return false } - if (buyResult?.encodedDeliveryToken.isNullOrBlank()) { + if (buyResult?.deliveryToken.isNullOrBlank()) { Log.e(TAG, "Auto-purchasing $packageName failed. Was the purchase rejected by the server?") return false } else { diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt index 1b1eb9b8ec..d5b1c4b24c 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt @@ -3,10 +3,10 @@ package org.microg.vending.billing.core import android.content.Context import android.util.Base64 import android.util.Log -import com.android.vending.Timestamp import org.json.JSONObject import org.microg.gms.utils.toBase64 import org.microg.vending.billing.proto.* +import org.microg.vending.proto.Timestamp import java.io.IOException import java.util.concurrent.TimeUnit @@ -93,7 +93,7 @@ class IAPCore( val requestBody = skuDetailsRequest.encode() val cacheEntry = skuDetailsCache.get(requestBody) if (cacheEntry != null) { - val getSkuDetailsResult = GetSkuDetailsResult.parseFrom(ResponseWrapper.ADAPTER.decode(cacheEntry).payload?.skuDetailsResponse) + val getSkuDetailsResult = GetSkuDetailsResult.parseFrom(GoogleApiResponse.ADAPTER.decode(cacheEntry).payload?.skuDetailsResponse) if (getSkuDetailsResult.skuDetailsList != null && getSkuDetailsResult.skuDetailsList.isNotEmpty()) { Log.d("IAPCore", "getSkuDetails from cache ") return getSkuDetailsResult @@ -104,7 +104,7 @@ class IAPCore( GooglePlayApi.URL_SKU_DETAILS, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), payload = skuDetailsRequest, - adapter = ResponseWrapper.ADAPTER + adapter = GoogleApiResponse.ADAPTER ) skuDetailsCache.put(requestBody, response.encode()) GetSkuDetailsResult.parseFrom(response.payload?.skuDetailsResponse) @@ -245,7 +245,7 @@ class IAPCore( headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), params = mapOf("theme" to acquireRequest.theme.toString()), payload = acquireRequest, - ResponseWrapper.ADAPTER + GoogleApiResponse.ADAPTER ) AcquireResult.parseFrom(params, acquireRequest, response.payload?.acquireResponse) } catch (e: Exception) { @@ -269,7 +269,7 @@ class IAPCore( GooglePlayApi.URL_CONSUME_PURCHASE, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), form = request, - adapter = ResponseWrapper.ADAPTER + adapter = GoogleApiResponse.ADAPTER ) ConsumePurchaseResult.parseFrom(response.payload?.consumePurchaseResponse) } catch (e: Exception) { @@ -290,7 +290,7 @@ class IAPCore( GooglePlayApi.URL_ACKNOWLEDGE_PURCHASE, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), payload = acknowledgePurchaseRequest, - adapter = ResponseWrapper.ADAPTER + adapter = GoogleApiResponse.ADAPTER ) AcknowledgePurchaseResult.parseFrom(response.payload?.acknowledgePurchaseResponse) } catch (e: Exception) { @@ -318,7 +318,7 @@ class IAPCore( GooglePlayApi.URL_GET_PURCHASE_HISTORY, HeaderProvider.getDefaultHeaders(authData, deviceInfo), reqParams, - ResponseWrapper.ADAPTER + GoogleApiResponse.ADAPTER ) GetPurchaseHistoryResult.parseFrom(response.payload?.purchaseHistoryResponse) } catch (e: IOException) { diff --git a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt index e6a3957f68..dcf8eeed12 100644 --- a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt +++ b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt @@ -32,7 +32,7 @@ suspend fun HttpClient.downloadPackageComponents( ) file }.onFailure { - Log.w(TAG, "package component failed to downlaod from url ${info.url}, " + + Log.w(TAG, "package component failed to download from url ${info.url}, " + "to be saved as `${info.componentName}`", it) }.getOrNull() } diff --git a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt index cffc1d82d0..97eddb5709 100644 --- a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt +++ b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt @@ -2,11 +2,11 @@ package org.microg.vending.delivery import android.util.Log import com.android.vending.buildRequestHeaders -import com.google.android.finsky.GoogleApiResponse import com.google.android.finsky.splitinstallservice.PackageComponent import org.microg.vending.billing.core.AuthData import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DELIVERY import org.microg.vending.billing.core.HttpClient +import org.microg.vending.billing.proto.GoogleApiResponse import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG private const val TAG = "GmsVendingDelivery" @@ -60,24 +60,24 @@ suspend fun HttpClient.requestDownloadUrls( ) Log.d(TAG, "requestDownloadUrls end response -> $response") - val basePackage = response.response!!.splitReqResult!!.pkgList?.let { + val basePackage = response.payload!!.deliveryResponse!!.deliveryData?.let { if (it.baseUrl != null && it.baseBytes != null) { PackageComponent(packageName, "base", it.baseUrl, it.baseBytes) } else null } - val splitComponents = response.response.splitReqResult!!.pkgList!!.pkgDownLoadInfo.filter { - !it.splitPkgName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() + val splitComponents = response.payload.deliveryResponse!!.deliveryData!!.splitPackages.filter { + !it.splitPackageName.isNullOrEmpty() && !it.downloadUrl.isNullOrEmpty() }.map { if (requestSplitPackages != null) { // Only download requested, if specific components were requested requestSplitPackages.firstOrNull { requestComponent -> - requestComponent.contains(it.splitPkgName!!) + requestComponent.contains(it.splitPackageName!!) }?.let { requestComponent -> PackageComponent(packageName, requestComponent, it.downloadUrl!!, it.size!!) } } else { // Download all offered components (server chooses) - PackageComponent(packageName, it.splitPkgName!!, it.downloadUrl!!, it.size!!) + PackageComponent(packageName, it.splitPackageName!!, it.downloadUrl!!, it.size!!) } } diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt index a80864b590..4306d3adfa 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/EnterpriseApp.kt @@ -1,6 +1,6 @@ package org.microg.vending.enterprise -import com.google.android.finsky.AppInstallPolicy +import org.microg.vending.enterprise.proto.AppInstallPolicy class EnterpriseApp( packageName: String, diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 51202c7b38..8657bdc03d 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -20,15 +20,9 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import com.android.vending.AppMeta -import com.android.vending.GetItemsRequest -import com.android.vending.GetItemsResponse -import com.android.vending.RequestApp -import com.android.vending.RequestItem import com.android.vending.buildRequestHeaders import com.android.vending.installer.installPackages import com.android.volley.VolleyError -import com.google.android.finsky.GoogleApiResponse import com.android.vending.installer.uninstallPackage import kotlinx.coroutines.runBlocking import org.microg.gms.common.DeviceConfiguration @@ -45,10 +39,15 @@ import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_FDFE import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo +import org.microg.vending.billing.proto.GoogleApiResponse import org.microg.vending.delivery.downloadPackageComponents import org.microg.vending.enterprise.EnterpriseApp import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.enterprise.AppState +import org.microg.vending.proto.AppMeta +import org.microg.vending.proto.GetItemsRequest +import org.microg.vending.proto.RequestApp +import org.microg.vending.proto.RequestItem import org.microg.vending.ui.components.EnterpriseList import org.microg.vending.ui.components.NetworkState import java.io.IOException @@ -178,14 +177,14 @@ class VendingActivity : ComponentActivity() { ), adapter = GoogleApiResponse.ADAPTER ) - Log.d(TAG, "uploaddc: ${upload.response!!.uploadDeviceConfigResponse}") + Log.d(TAG, "uploaddc: ${upload.payload!!.uploadDeviceConfigResponse}") // Fetch list of apps available to the scoped enterprise account val apps = client.post( url = URL_ENTERPRISE_CLIENT_POLICY, headers = headers.plus("content-type" to "application/x-protobuf"), adapter = GoogleApiResponse.ADAPTER - ).response?.enterpriseClientPolicyResult?.policy?.apps?.filter { it.packageName != null && it.policy != null } + ).payload?.enterpriseClientPolicyResponse?.policy?.apps?.filter { it.packageName != null && it.policy != null } if (apps == null) { Log.e(TAG, "unexpected network response: missing expected fields") @@ -200,13 +199,13 @@ class VendingActivity : ComponentActivity() { url = URL_ITEM_DETAILS, // TODO: meaning unclear, but returns 400 without. constant? possibly has influence on which fields are returned? headers = headers.plus("x-dfe-item-field-mask" to "GgWGHay3ByILPP/Avy+4A4YlCRM"), - adapter = GetItemsResponse.ADAPTER, payload = GetItemsRequest( apps.map { RequestItem(RequestApp(AppMeta(it.packageName))) } - ) - ).items.map { it.response }.filterNotNull().associate { item -> + ), + adapter = GoogleApiResponse.ADAPTER + ).getItemsResponses.mapNotNull { it.response }.associate { item -> val packageName = item.meta!!.packageName!! val installedDetails = this@VendingActivity.packageManager.getInstalledPackages(0).find { it.applicationInfo.packageName == packageName diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index 900e08815e..bedec8d59c 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -24,10 +24,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.android.vending.R -import com.google.android.finsky.AppInstallPolicy -import org.microg.vending.enterprise.App import org.microg.vending.enterprise.AppState import org.microg.vending.enterprise.EnterpriseApp +import org.microg.vending.enterprise.proto.AppInstallPolicy @Composable diff --git a/vending-app/src/main/kotlin/com/android/vending/extensions.kt b/vending-app/src/main/kotlin/com/android/vending/extensions.kt index b0c51f94ae..9efdc3804c 100644 --- a/vending-app/src/main/kotlin/com/android/vending/extensions.kt +++ b/vending-app/src/main/kotlin/com/android/vending/extensions.kt @@ -11,10 +11,29 @@ 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 org.microg.vending.billing.getUserAgent +import org.microg.vending.proto.AndroidVersionMeta +import org.microg.vending.proto.DeviceMeta +import org.microg.vending.proto.EncodedTriple +import org.microg.vending.proto.EncodedTripleWrapper +import org.microg.vending.proto.IntWrapper +import org.microg.vending.proto.Locality +import org.microg.vending.proto.LocalityWrapper +import org.microg.vending.proto.RequestHeader +import org.microg.vending.proto.RequestLanguagePackage +import org.microg.vending.proto.StringWrapper +import org.microg.vending.proto.Timestamp +import org.microg.vending.proto.TimestampContainer +import org.microg.vending.proto.TimestampContainer1 +import org.microg.vending.proto.TimestampContainer1Wrapper +import org.microg.vending.proto.TimestampContainer2 +import org.microg.vending.proto.TimestampStringWrapper +import org.microg.vending.proto.TimestampWrapper +import org.microg.vending.proto.UnknownByte12 +import org.microg.vending.proto.UserAgent +import org.microg.vending.proto.Uuid import java.io.ByteArrayOutputStream import java.io.IOException import java.net.URLEncoder diff --git a/vending-app/src/main/proto/DeliveryResponse.proto b/vending-app/src/main/proto/DeliveryResponse.proto new file mode 100644 index 0000000000..8e4246e74c --- /dev/null +++ b/vending-app/src/main/proto/DeliveryResponse.proto @@ -0,0 +1,58 @@ + +option java_package = "org.microg.vending.delivery.proto"; +option java_multiple_files = true; + +message DeliveryResponse { + optional DeliveryStatus status = 1; + optional DeliveryData deliveryData = 2; +} + +enum DeliveryStatus { + SUCCESS = 1; + NOT_SUPPORTED = 2; + NOT_PURCHASED = 3; + APP_REMOVED = 7; + APP_NOT_SUPPORTED = 9; +} + +message DeliveryData { + /* + * Size of the file downloaded through `baseUrl` in bytes. + */ + optional uint32 baseBytes = 1; + /* + * Location of `base.apk`. + */ + optional string baseUrl = 3; + repeated SplitDeliveryData splitPackages = 15; +} + +/* + * Encodes additional app bundle components (according to observations, both + * OBB and split APK files). + */ +message SplitDeliveryData { + optional string splitPackageName = 1; + /* + * Size of the package described by this message, i.e. the file at `downloadUrl`. + */ + optional uint32 size = 2; + optional string sha1 = 4; + optional string downloadUrl = 5; + /* + * Alternative download? Meaning not clear, unused. + */ + optional DownloadInfo downloadInfo1 = 8; + optional string sha256 = 9; + optional string unknownInfoString = 15; + /* + * Alternative download? Meaning not clear, unused. + */ + optional DownloadInfo downloadInfo2 = 16; +} + +message DownloadInfo { + optional int32 id = 1; + optional uint32 bytes = 2; + optional string url = 3; +} diff --git a/vending-app/src/main/proto/EnterpriseClientPolicy.proto b/vending-app/src/main/proto/EnterpriseClientPolicy.proto index 7d0095b8ea..93de641cb4 100644 --- a/vending-app/src/main/proto/EnterpriseClientPolicy.proto +++ b/vending-app/src/main/proto/EnterpriseClientPolicy.proto @@ -1,7 +1,6 @@ - syntax = "proto3"; -option java_package = "com.google.android.finsky"; +option java_package = "org.microg.vending.enterprise.proto"; option java_multiple_files = true; message EnterpriseClientPolicyResponse { @@ -20,6 +19,7 @@ message App { optional int32 unknownNumber = 9; // = 1 } +// TODO: could be inaccurate enum AppInstallPolicy { UNKNOWN = 0; OPTIONAL = 1; diff --git a/vending-app/src/main/proto/GetItemsRequest.proto b/vending-app/src/main/proto/GetItemsRequest.proto index c9659f6154..0b785e1321 100644 --- a/vending-app/src/main/proto/GetItemsRequest.proto +++ b/vending-app/src/main/proto/GetItemsRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "com.android.vending"; +option java_package = "org.microg.vending.proto"; option java_multiple_files = true; message GetItemsRequest { diff --git a/vending-app/src/main/proto/GetItemsResponse.proto b/vending-app/src/main/proto/GetItemsResponse.proto index c644a4703c..2ea38871c6 100644 --- a/vending-app/src/main/proto/GetItemsResponse.proto +++ b/vending-app/src/main/proto/GetItemsResponse.proto @@ -1,16 +1,11 @@ syntax = "proto3"; -import "GetItemsRequest.proto"; -option java_package = "com.android.vending"; +option java_package = "org.microg.vending.proto"; option java_multiple_files = true; -message GetItemsResponse { - // technically this is a typical Google response containing the typical `GoogleApiResponse` fields, - // but field 1.145 only encodes some kind of index and the real content is at field 11 - repeated ItemQuery items = 11; -} +import "GetItemsRequest.proto"; -message ItemQuery { +message GetItemsResponse { optional RequestApp query = 1; optional ItemResponse response = 2; } diff --git a/vending-app/src/main/proto/GooglePlayResponse.proto b/vending-app/src/main/proto/GooglePlayResponse.proto new file mode 100644 index 0000000000..98d12d114b --- /dev/null +++ b/vending-app/src/main/proto/GooglePlayResponse.proto @@ -0,0 +1,60 @@ +syntax = "proto3"; + +option java_package = "org.microg.vending.billing.proto"; +option java_multiple_files = true; + +import "EnterpriseClientPolicy.proto"; +import "DeliveryResponse.proto"; +import "LicenseResult.proto"; +import "UploadDeviceConfigResponse.proto"; +import "GetItemsResponse.proto"; +import "Purchase.proto"; + +message GoogleApiResponse { + optional Payload payload = 1; + optional ServerCommands commands = 2; + repeated PreFetch preFetch = 3; + optional ServerMeta meta = 5; + + optional bytes serverLogsCookie = 9; // not used + + /* + * For getItems queries, field 1.145 only encodes some kind of index. The + * real content is here, at field 11. + */ + repeated GetItemsResponse getItemsResponses = 11; +} + +message Payload { + optional DetailsResponse detailsResponse = 2; + optional BuyResponse buyResponse = 4; + optional DeliveryResponse deliveryResponse = 21; + optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 28; + optional ConsumePurchaseResponse consumePurchaseResponse = 30; + optional PurchaseHistoryResponse purchaseHistoryResponse = 67; + optional LicenseCheckV1Response licenseV1Response = 76; + optional SkuDetailsResponse skuDetailsResponse = 82; + optional AcquireResponse acquireResponse = 94; + optional EnterpriseClientPolicyResponse enterpriseClientPolicyResponse = 135; + optional AcknowledgePurchaseResponse acknowledgePurchaseResponse = 140; + optional LicenseCheckV2Response licenseV2Response = 173; + // optional SyncApiResp syncResult = 183; +} + +message ServerCommands { + bool clearCache = 1; + string displayErrorMessage = 2; + string logErrorStacktrace = 3; +} + +message PreFetch { + string url = 1; + GoogleApiResponse response = 2; + string etag = 3; + int64 ttl = 4; + int64 softTtl = 5; +} + +message ServerMeta { + optional int64 latencyMillis = 1; +} \ No newline at end of file diff --git a/vending-app/src/main/proto/LicenseResult.proto b/vending-app/src/main/proto/LicenseResult.proto index 238aa263f6..4681488be0 100644 --- a/vending-app/src/main/proto/LicenseResult.proto +++ b/vending-app/src/main/proto/LicenseResult.proto @@ -1,25 +1,16 @@ syntax = "proto2"; -option java_package = "com.android.vending"; +option java_package = "org.microg.vending.licensing.proto"; option java_multiple_files = true; -message LicenseResult { - optional LicenseInformation information = 1; -} - -message LicenseInformation { - optional V1Container v1 = 76; - optional V2Container v2 = 173; -} - -message V1Container { +message LicenseCheckV1Response { optional uint32 result = 1; optional string signedData = 2; optional string signature = 3; } -message V2Container { +message LicenseCheckV2Response { optional AppLicense license = 1; } diff --git a/vending-app/src/main/proto/Locality.proto b/vending-app/src/main/proto/Locality.proto index 8e74fbcf69..3d606a74f9 100644 --- a/vending-app/src/main/proto/Locality.proto +++ b/vending-app/src/main/proto/Locality.proto @@ -1,6 +1,6 @@ syntax = "proto2"; -option java_package = "com.android.vending"; +option java_package = "org.microg.vending.proto"; option java_multiple_files = true; import "Timestamp.proto"; diff --git a/vending-app/src/main/proto/GooglePlay.proto b/vending-app/src/main/proto/Purchase.proto similarity index 93% rename from vending-app/src/main/proto/GooglePlay.proto rename to vending-app/src/main/proto/Purchase.proto index 49fd299806..e83541eda0 100644 --- a/vending-app/src/main/proto/GooglePlay.proto +++ b/vending-app/src/main/proto/Purchase.proto @@ -266,30 +266,12 @@ message DynamicSku { string unknown3 = 3; } -message ResponseWrapper { - Payload payload = 1; - ServerCommands commands = 2; - repeated PreFetch preFetch = 3; - ServerMetadata serverMetadata = 5; - bytes serverLogsCookie = 9; -} - -message Payload { - DetailsResponse detailsResponse = 2; - BuyResponse buyResponse = 4; - ConsumePurchaseResponse consumePurchaseResponse = 30; - PurchaseHistoryResponse purchaseHistoryResponse = 67; - SkuDetailsResponse skuDetailsResponse = 82; - AcquireResponse acquireResponse = 94; - AcknowledgePurchaseResponse acknowledgePurchaseResponse = 140; -} - message DetailsResponse { Item item = 4; } message BuyResponse { - string encodedDeliveryToken = 55; + string deliveryToken = 55; } message Item { @@ -698,22 +680,4 @@ message PurchaseHistoryResponse { repeated string signature = 3; string continuationToken = 4; FailedResponse failedResponse = 5; -} - -message ServerCommands { - bool clearCache = 1; - string displayErrorMessage = 2; - string logErrorStacktrace = 3; -} - -message PreFetch { - string url = 1; - ResponseWrapper response = 2; - string etag = 3; - int64 ttl = 4; - int64 softTtl = 5; -} - -message ServerMetadata { - int64 latencyMillis = 1; } \ No newline at end of file diff --git a/vending-app/src/main/proto/RequestHeader.proto b/vending-app/src/main/proto/RequestHeader.proto index 2dea2e09d4..ae13f73e16 100644 --- a/vending-app/src/main/proto/RequestHeader.proto +++ b/vending-app/src/main/proto/RequestHeader.proto @@ -1,6 +1,6 @@ syntax = "proto2"; -option java_package = "com.android.vending"; +option java_package = "org.microg.vending.proto"; option java_multiple_files = true; message RequestHeader { diff --git a/vending-app/src/main/proto/SplitInstall.proto b/vending-app/src/main/proto/SplitInstall.proto deleted file mode 100644 index 48b1c8eb4a..0000000000 --- a/vending-app/src/main/proto/SplitInstall.proto +++ /dev/null @@ -1,96 +0,0 @@ - -option java_package = "com.google.android.finsky"; -option java_multiple_files = true; - -import "EnterpriseClientPolicy.proto"; - -message GoogleApiResponse { - optional ApiResponse response = 1; - optional UnknownType type= 5; - optional bytes unknownFieldBytes= 9; -} - -message UnknownType { - optional int64 id=1; -} - -message ApiResponse { - optional SplitResponse splitReqResult = 21; - optional UploadDeviceConfigResponse uploadDeviceConfigResponse = 28; - optional EnterpriseClientPolicyResponse enterpriseClientPolicyResult = 135; -// optional SyncApiResp syncResult = 183; -} - -message UploadDeviceConfigResponse { - optional string deviceConfigToken = 1; -} - -message SplitResponse { - optional DeliveryStatus status = 1; - optional PkgFetchInfo pkgList = 2; -} - -enum DeliveryStatus { - SUCCESS = 1; - NOT_SUPPORTED = 2; - NOT_PURCHASED = 3; - APP_REMOVED = 7; - APP_NOT_SUPPORTED = 9; -} - -message PkgFetchInfo { - optional uint32 baseBytes = 1; - optional string baseUrl = 3; - repeated SplitPkgInfo pkgDownLoadInfo = 15; -} - -message SplitPkgInfo { - optional string splitPkgName = 1; - optional uint32 size = 2; - optional string checkSum = 4; - optional string downloadUrl = 5; - optional DownloadInfo slaveDownloadInfo = 8; - optional string checksum = 9; - optional string unknownPkgInfoString = 15; - optional DownloadInfo otherDownloadInfo = 16; -} - -message DownloadInfo { - optional int32 id = 1; - optional uint32 bytes = 2; - optional string url = 3; -} - -//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; -//} - diff --git a/vending-app/src/main/proto/Timestamp.proto b/vending-app/src/main/proto/Timestamp.proto index fa98206e11..626b847202 100644 --- a/vending-app/src/main/proto/Timestamp.proto +++ b/vending-app/src/main/proto/Timestamp.proto @@ -1,6 +1,6 @@ syntax = "proto2"; -option java_package = "com.android.vending"; +option java_package = "org.microg.vending.proto"; option java_multiple_files = true; message TimestampContainer { diff --git a/vending-app/src/main/proto/UploadDeviceConfigResponse.proto b/vending-app/src/main/proto/UploadDeviceConfigResponse.proto new file mode 100644 index 0000000000..905b4f370c --- /dev/null +++ b/vending-app/src/main/proto/UploadDeviceConfigResponse.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +option java_package = "org.microg.vending.proto"; + +message UploadDeviceConfigResponse { + optional string deviceConfigToken = 1; +} \ No newline at end of file From ec18cdbe85acb0fb461d2df98fcb15693c896082 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 25 Sep 2024 16:04:39 +0200 Subject: [PATCH 46/59] Replace volley with ktor + okhttp Allows downloading large applications --- build.gradle | 2 + vending-app/build.gradle | 6 +- vending-app/proguard-rules.pro | 6 + .../vending/licensing/LicenseChecker.kt | 6 +- .../vending/licensing/LicensingService.kt | 6 +- .../vending/billing/AcquireFreeAppLicense.kt | 10 +- .../microg/vending/billing/core/HttpClient.kt | 243 ++++++++++-------- .../microg/vending/billing/core/IAPCore.kt | 12 +- .../billing/ui/logic/InAppBillingViewModel.kt | 6 +- .../vending/delivery/ComponentDownload.kt | 6 +- .../org/microg/vending/ui/VendingActivity.kt | 9 +- .../SplitInstallManager.kt | 4 +- 12 files changed, 162 insertions(+), 154 deletions(-) create mode 100644 vending-app/proguard-rules.pro diff --git a/build.gradle b/build.gradle index 5e9d0ecedc..1248b41999 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,8 @@ buildscript { ext.slf4jVersion = '1.7.36' ext.volleyVersion = '1.2.1' + ext.okHttpVersion = '4.12.0' + ext.ktorVersion = '2.3.12' ext.wireVersion = '4.9.9' ext.androidBuildGradleVersion = '8.2.2' diff --git a/vending-app/build.gradle b/vending-app/build.gradle index b3cceb42d2..3cbd74178b 100644 --- a/vending-app/build.gradle +++ b/vending-app/build.gradle @@ -26,6 +26,7 @@ android { removeUnusedResources false obfuscate false optimizeCode false + proguardFile 'proguard-rules.pro' } } release { @@ -34,6 +35,7 @@ android { removeUnusedResources true obfuscate false optimizeCode true + proguardFile 'proguard-rules.pro' } } } @@ -96,7 +98,9 @@ dependencies { implementation "com.squareup.wire:wire-runtime:$wireVersion" implementation "com.squareup.wire:wire-grpc-client:$wireVersion" - implementation "com.android.volley:volley:$volleyVersion" + implementation "com.squareup.okhttp3:okhttp:$okHttpVersion" + implementation "io.ktor:ktor-client-core:$ktorVersion" + implementation "io.ktor:ktor-client-okhttp:$ktorVersion" implementation "androidx.webkit:webkit:$webkitVersion" diff --git a/vending-app/proguard-rules.pro b/vending-app/proguard-rules.pro new file mode 100644 index 0000000000..395bd2608c --- /dev/null +++ b/vending-app/proguard-rules.pro @@ -0,0 +1,6 @@ +# OKHttp rules +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** +-dontwarn org.slf4j.impl.StaticLoggerBinder \ No newline at end of file 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 c180e805a2..735ba57d35 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 @@ -10,7 +10,6 @@ import android.util.Log import com.android.vending.AUTH_TOKEN_SCOPE import com.android.vending.buildRequestHeaders import com.android.vending.getAuthToken -import com.android.volley.VolleyError import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.proto.GoogleApiResponse import java.io.IOException @@ -126,11 +125,8 @@ suspend fun HttpClient.checkLicense( packageName, auth, packageInfo.versionCode, decodedAndroidId ) } ?: ErrorResponse(NOT_LICENSED) - } catch (e: VolleyError) { - Log.e(TAG, "License request failed with $e") - ErrorResponse(ERROR_CONTACTING_SERVER) } catch (e: IOException) { - Log.e(TAG, "Encountered a network error during operation ($e)") + Log.e(TAG, "Encountered a network error during operation", e) ErrorResponse(ERROR_CONTACTING_SERVER) } catch (e: OperationCanceledException) { ErrorResponse(ERROR_CONTACTING_SERVER) diff --git a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt index 47c5afcce3..cdf9cca632 100644 --- a/vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt +++ b/vending-app/src/main/java/com/android/vending/licensing/LicensingService.kt @@ -16,8 +16,6 @@ import android.os.RemoteException import android.util.Log import com.android.vending.VendingPreferences.isLicensingEnabled import com.android.vending.VendingPreferences.isLicensingPurchaseFreeAppsEnabled -import com.android.volley.RequestQueue -import com.android.volley.toolbox.Volley import kotlinx.coroutines.runBlocking import org.microg.gms.auth.AuthConstants import org.microg.gms.profile.ProfileManager.ensureInitialized @@ -25,7 +23,6 @@ import org.microg.vending.billing.acquireFreeAppLicense import org.microg.vending.billing.core.HttpClient class LicensingService : Service() { - private lateinit var queue: RequestQueue private lateinit var accountManager: AccountManager private lateinit var androidId: String private lateinit var httpClient: HttpClient @@ -199,9 +196,8 @@ class LicensingService : Service() { androidId = java.lang.Long.toHexString(cursor.getLong(0)) } } - queue = Volley.newRequestQueue(this) accountManager = AccountManager.get(this) - httpClient = HttpClient(this) + httpClient = HttpClient() return mLicenseService } diff --git a/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt b/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt index 0f28237f9a..8899fa1090 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/AcquireFreeAppLicense.kt @@ -3,7 +3,7 @@ package org.microg.vending.billing import android.accounts.Account import android.content.Context import android.util.Log -import com.android.volley.VolleyError +import io.ktor.utils.io.errors.IOException import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_DETAILS import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_PURCHASE import org.microg.vending.billing.core.HeaderProvider @@ -29,8 +29,8 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, params = mapOf("doc" to packageName), adapter = GoogleApiResponse.ADAPTER ).payload?.detailsResponse - } catch (e: VolleyError) { - Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response when gathering app data") + } catch (e: IOException) { + Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response when gathering app data", e) return false } @@ -67,8 +67,8 @@ suspend fun HttpClient.acquireFreeAppLicense(context: Context, account: Account, params = parameters, adapter = GoogleApiResponse.ADAPTER ).payload?.buyResponse - } catch (e: VolleyError) { - Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase") + } catch (e: IOException) { + Log.e(TAG, "Unable to auto-purchase $packageName because of a network error or unexpected response during purchase", e) return false } 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 aef4b64244..402a616a23 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 @@ -1,55 +1,58 @@ package org.microg.vending.billing.core -import android.content.Context -import android.net.Uri -import com.android.volley.* -import com.android.volley.toolbox.HttpHeaderParser -import com.android.volley.toolbox.JsonObjectRequest -import com.android.volley.toolbox.Volley import com.squareup.wire.Message import com.squareup.wire.ProtoAdapter +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.timeout +import io.ktor.client.request.forms.submitForm +import io.ktor.client.request.get +import io.ktor.client.request.headers +import io.ktor.client.request.post +import io.ktor.client.request.prepareGet +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.ParametersImpl +import io.ktor.http.URLBuilder +import io.ktor.http.Url +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.jvm.javaio.copyTo 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 -private const val POST_TIMEOUT = 8000 +private const val POST_TIMEOUT = 8000L -class HttpClient(context: Context) { +class HttpClient { - val requestQueue = singleInstanceOf { Volley.newRequestQueue(context.applicationContext) } + private val client = singleInstanceOf { HttpClient(OkHttp) { + expectSuccess = true + //install(HttpCache) + install(HttpTimeout) + } } - suspend fun download(url: String, downloadFile: File, tag: Any): String = suspendCoroutine { continuation -> - val uriBuilder = Uri.parse(url).buildUpon() - requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) { - override fun parseNetworkResponse(response: NetworkResponse): Response { - 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) + suspend fun download( + url: String, + downloadFile: File, + params: Map = emptyMap() + ): File { + client.prepareGet(url.asUrl(params)).execute { response -> + val parentDir = downloadFile.getParentFile() + if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { + throw IOException("Failed to create directories: ${parentDir.absolutePath}") } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) + val body: ByteReadChannel = response.body() + FileOutputStream(downloadFile).use { out -> + body.copyTo(out = out) } - }.setShouldCache(false).setTag(tag)) + } + return downloadFile } suspend fun get( @@ -57,117 +60,129 @@ class HttpClient(context: Context) { headers: Map = emptyMap(), params: Map = emptyMap(), adapter: ProtoAdapter, - cache: Boolean = true - ): O = suspendCoroutine { continuation -> - val uriBuilder = Uri.parse(url).buildUpon() - params.forEach { - uriBuilder.appendQueryParameter(it.key, it.value) - } - requestQueue.add(object : Request(Method.GET, uriBuilder.build().toString(), null) { - override fun parseNetworkResponse(response: NetworkResponse): Response { - if (response.statusCode != 200) throw VolleyError(response) - return Response.success(adapter.decode(response.data), HttpHeaderParser.parseCacheHeaders(response)) - } + ): O { - override fun deliverResponse(response: O) { - continuation.resume(response) - } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) + val response = client.get(url.asUrl(params)) { + headers { + headers.forEach { + append(it.key, it.value) + } } - - override fun getHeaders(): Map = headers - }.setShouldCache(cache)) + } + if (response.status != HttpStatusCode.OK) throw IOException("Server responded with status ${response.status}") + else return adapter.decode(response.body()) } + /** + * Post empty body. + */ + suspend fun , O> post( + url: String, + headers: Map = emptyMap(), + params: Map = emptyMap(), + adapter: ProtoAdapter, + cache: Boolean = false // TODO not implemented + ): O { + val response = client.post(url.asUrl(params)) { + setBody(ByteArray(0)) + headers { + headers.forEach { + append(it.key, it.value) + } + append(HttpHeaders.ContentType, "application/x-protobuf") + } + timeout { + requestTimeoutMillis = POST_TIMEOUT + } + } + return adapter.decode(response.body()) + } + /** + * Post protobuf-encoded body. + */ suspend fun , O> post( url: String, headers: Map = emptyMap(), params: Map = emptyMap(), payload: I, adapter: ProtoAdapter, - cache: Boolean = false - ): O = suspendCoroutine { continuation -> - val uriBuilder = Uri.parse(url).buildUpon() - params.forEach { - uriBuilder.appendQueryParameter(it.key, it.value) - } - requestQueue.add(object : Request(Method.POST, uriBuilder.build().toString(), null) { - override fun parseNetworkResponse(response: NetworkResponse): Response { - if (response.statusCode != 200) throw VolleyError(response) - return Response.success(adapter.decode(response.data), HttpHeaderParser.parseCacheHeaders(response)) - } + cache: Boolean = false // TODO not implemented + ): O { + val response = client.post(url.asUrl(params)) { + setBody(ByteReadChannel(payload.encode())) + headers { + headers.forEach { + append(it.key, it.value) + } - override fun deliverResponse(response: O) { - continuation.resume(response) + append(HttpHeaders.ContentType, "application/x-protobuf") } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) + timeout { + requestTimeoutMillis = POST_TIMEOUT } - - override fun getHeaders(): Map = headers - override fun getBody(): ByteArray = payload.encode() - override fun getBodyContentType(): String = "application/x-protobuf" - }.setShouldCache(cache).setRetryPolicy(DefaultRetryPolicy(POST_TIMEOUT, 0, 0.0F))) + } + return adapter.decode(response.body()) } + /** + * Post JSON body. + */ suspend fun post( url: String, headers: Map = emptyMap(), params: Map = emptyMap(), payload: JSONObject, - cache: Boolean = false - ): JSONObject = suspendCoroutine { continuation -> - val uriBuilder = Uri.parse(url).buildUpon() - params.forEach { - uriBuilder.appendQueryParameter(it.key, it.value) - } - requestQueue.add(object : JsonObjectRequest(Method.POST, uriBuilder.build().toString(), payload, null, null) { + cache: Boolean = false // TODO not implemented + ): JSONObject { + val response = client.post(url.asUrl(params)) { + setBody(payload.toString()) + headers { + headers.forEach { + append(it.key, it.value) + } - override fun deliverResponse(response: JSONObject) { - continuation.resume(response) + append(HttpHeaders.ContentType, "application/json") } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) + timeout { + requestTimeoutMillis = POST_TIMEOUT } - - override fun getHeaders(): Map = headers - }.setShouldCache(cache).setRetryPolicy(DefaultRetryPolicy(POST_TIMEOUT, 0, 0.0F))) + } + return JSONObject(response.body()) } + /** + * Post form body. + */ suspend fun post( url: String, headers: Map = emptyMap(), params: Map = emptyMap(), form: Map = emptyMap(), adapter: ProtoAdapter, - cache: Boolean = false - ): O = suspendCoroutine { continuation -> - val uriBuilder = Uri.parse(url).buildUpon() - params.forEach { - uriBuilder.appendQueryParameter(it.key, it.value) - } - requestQueue.add(object : Request(Method.POST, uriBuilder.build().toString(), null) { - override fun parseNetworkResponse(response: NetworkResponse): Response { - if (response.statusCode != 200) throw VolleyError(response) - return Response.success(adapter.decode(response.data), HttpHeaderParser.parseCacheHeaders(response)) - } - - override fun deliverResponse(response: O) { - continuation.resume(response) + cache: Boolean = false // TODO not implemented + ): O { + val response = client.submitForm( + formParameters = ParametersImpl(form.mapValues { listOf(it.key) }), + encodeInQuery = false + ) { + url(url.asUrl(params)) + headers { // Content-Type is set to `x-www-form-urlencode` automatically + headers.forEach { + append(it.key, it.value) + } } - - override fun deliverError(error: VolleyError) { - continuation.resumeWithException(error) + timeout { + requestTimeoutMillis = POST_TIMEOUT } - - override fun getHeaders(): Map = headers - override fun getParams(): Map = form - }.setShouldCache(cache).setRetryPolicy(DefaultRetryPolicy(POST_TIMEOUT, 0, 0.0F))) + } + return adapter.decode(response.body()) } + + private fun String.asUrl(params: Map): Url = URLBuilder(this).apply { + params.forEach { + parameters.append(it.key, it.value) + } + }.build() } \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt index d5b1c4b24c..b25bdf100b 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/core/IAPCore.kt @@ -19,7 +19,7 @@ class IAPCore( private val authData: AuthData ) { suspend fun requestAuthProofToken(password: String): String { - return HttpClient(context).post( + return HttpClient().post( GooglePlayApi.URL_AUTH_PROOF_TOKENS, headers = HeaderProvider.getBaseHeaders(authData, deviceInfo), payload = JSONObject().apply { @@ -100,7 +100,7 @@ class IAPCore( } } Log.d("IAPCore", "getSkuDetails: ") - val response = HttpClient(context).post( + val response = HttpClient().post( GooglePlayApi.URL_SKU_DETAILS, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), payload = skuDetailsRequest, @@ -240,7 +240,7 @@ class IAPCore( }.build() } return try { - val response = HttpClient(context).post( + val response = HttpClient().post( GooglePlayApi.URL_EES_ACQUIRE, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), params = mapOf("theme" to acquireRequest.theme.toString()), @@ -265,7 +265,7 @@ class IAPCore( ) return try { - val response = HttpClient(context).post( + val response = HttpClient().post( GooglePlayApi.URL_CONSUME_PURCHASE, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), form = request, @@ -286,7 +286,7 @@ class IAPCore( }.build() return try { - val response = HttpClient(context).post( + val response = HttpClient().post( GooglePlayApi.URL_ACKNOWLEDGE_PURCHASE, headers = HeaderProvider.getDefaultHeaders(authData, deviceInfo), payload = acknowledgePurchaseRequest, @@ -314,7 +314,7 @@ class IAPCore( } return try { - val response = HttpClient(context).get( + val response = HttpClient().get( GooglePlayApi.URL_GET_PURCHASE_HISTORY, HeaderProvider.getDefaultHeaders(authData, deviceInfo), reqParams, diff --git a/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt b/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt index 969bcd123d..26f034d792 100644 --- a/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt +++ b/vending-app/src/main/java/org/microg/vending/billing/ui/logic/InAppBillingViewModel.kt @@ -16,7 +16,7 @@ import androidx.core.os.bundleOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.billingclient.api.BillingClient -import com.android.volley.VolleyError +import io.ktor.utils.io.errors.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay @@ -103,10 +103,8 @@ class InAppBillingViewModel : ViewModel() { val param = startParams!!.getString(KEY_IAP_SHEET_UI_PARAM) val (statusCode, encodedRapt) = try { 200 to InAppBillingServiceImpl.requestAuthProofToken(ContextProvider.context, param!!, password) - } catch (e: VolleyError) { - Log.w(TAG, e) - e.networkResponse.statusCode to null } catch (e: Exception) { + Log.w(TAG, e) -1 to null } if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "requestAuthProofToken statusCode=$statusCode, encodedRapt=$encodedRapt") diff --git a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt index dcf8eeed12..ab6a77dfef 100644 --- a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt +++ b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt @@ -24,13 +24,11 @@ suspend fun HttpClient.downloadPackageComponents( Log.d(TAG, "downloadSplitPackage: $info") async { info to runCatching { - val file = File(context.packageDownloadLocation().toString(), info.componentName) + val file = File(context.packageDownloadLocation().toString(), "${info.packageName}-${info.componentName}") download( url = info.url, - downloadFile = file, - tag = tag + downloadFile = file ) - file }.onFailure { Log.w(TAG, "package component failed to download from url ${info.url}, " + "to be saved as `${info.componentName}`", it) diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 8657bdc03d..94063c1c64 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.android.vending.buildRequestHeaders import com.android.vending.installer.installPackages -import com.android.volley.VolleyError import com.android.vending.installer.uninstallPackage import kotlinx.coroutines.runBlocking import org.microg.gms.common.DeviceConfiguration @@ -89,7 +88,7 @@ class VendingActivity : ComponentActivity() { val previousState = apps[app]!! apps[app] = AppState.PENDING - val client = HttpClient(this@VendingActivity) + val client = HttpClient() // Get download links for requested package val downloadUrls = runCatching { client.requestDownloadUrls( @@ -164,7 +163,7 @@ class VendingActivity : ComponentActivity() { } val headers = buildRequestHeaders(authData.authToken, authData.gsfId.toLong(16)) - val client = HttpClient(this@VendingActivity) + val client = HttpClient() // Register device for server-side compatibility checking val upload = client.post( @@ -244,10 +243,6 @@ class VendingActivity : ComponentActivity() { networkState = NetworkState.ERROR Log.e(TAG, "Network error: ${e.message}") e.printStackTrace() - } catch (e: VolleyError) { - networkState = NetworkState.ERROR - Log.e(TAG, "Network error: ${e.message}") - e.printStackTrace() } catch (e: NullPointerException) { networkState = NetworkState.ERROR Log.e(TAG, "Unexpected network response, cannot process") 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 d39146a5d4..60e98f47c3 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 @@ -29,7 +29,6 @@ import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG private const val SPLIT_INSTALL_NOTIFY_ID = 111 -private const val SPLIT_INSTALL_REQUEST_TAG = "splitInstallRequestTag" private const val NOTIFY_CHANNEL_ID = "splitInstall" private const val NOTIFY_CHANNEL_NAME = "Split Install" @@ -48,7 +47,7 @@ private const val TAG = "SplitInstallManager" class SplitInstallManager(val context: Context) { - private var httpClient: HttpClient = HttpClient(context) + private var httpClient: HttpClient = HttpClient() suspend fun splitInstallFlow(callingPackage: String, splits: List): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false @@ -181,7 +180,6 @@ class SplitInstallManager(val context: Context) { } fun release() { - httpClient.requestQueue.cancelAll(SPLIT_INSTALL_REQUEST_TAG) splitInstallRecord.clear() deferredMap.clear() } From 09acd1ad4ba17c617437d147accf8b6c7805ae96 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Wed, 25 Sep 2024 17:14:17 +0200 Subject: [PATCH 47/59] Install packages directly to improve performance * reduces temporary storage usage * speeds up installation by a little bit --- .../microg/vending/billing/core/HttpClient.kt | 25 +++++---- .../vending/delivery/ComponentDownload.kt | 38 -------------- .../org/microg/vending/ui/VendingActivity.kt | 20 +++---- .../com/android/vending/installer/Install.kt | 52 +++++++++++++++---- .../SplitInstallManager.kt | 26 ++++------ 5 files changed, 74 insertions(+), 87 deletions(-) delete mode 100644 vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt 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 402a616a23..a2a1d11224 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 @@ -26,6 +26,7 @@ import org.microg.gms.utils.singleInstanceOf import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.io.OutputStream private const val POST_TIMEOUT = 8000L @@ -41,18 +42,24 @@ class HttpClient { url: String, downloadFile: File, params: Map = emptyMap() - ): File { + ): File = downloadFile.also { toFile -> + val parentDir = downloadFile.getParentFile() + if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { + throw IOException("Failed to create directories: ${parentDir.absolutePath}") + } + + FileOutputStream(toFile).use { download(url, it, params) } + } + + suspend fun download( + url: String, + downloadTo: OutputStream, + params: Map = emptyMap() + ) { client.prepareGet(url.asUrl(params)).execute { response -> - val parentDir = downloadFile.getParentFile() - if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) { - throw IOException("Failed to create directories: ${parentDir.absolutePath}") - } val body: ByteReadChannel = response.body() - FileOutputStream(downloadFile).use { out -> - body.copyTo(out = out) - } + body.copyTo(out = downloadTo) } - return downloadFile } suspend fun get( diff --git a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt b/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt deleted file mode 100644 index ab6a77dfef..0000000000 --- a/vending-app/src/main/java/org/microg/vending/delivery/ComponentDownload.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.microg.vending.delivery - -import android.content.Context -import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import com.android.vending.installer.packageDownloadLocation -import com.google.android.finsky.splitinstallservice.PackageComponent -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import org.microg.vending.billing.core.HttpClient -import java.io.File - -private const val TAG = "GmsVendingComponentDl" - -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -suspend fun HttpClient.downloadPackageComponents( - context: Context, - downloadList: List, - tag: Any -): Map = coroutineScope { - downloadList.map { info -> - Log.d(TAG, "downloadSplitPackage: $info") - async { - info to runCatching { - val file = File(context.packageDownloadLocation().toString(), "${info.packageName}-${info.componentName}") - download( - url = info.url, - downloadFile = file - ) - }.onFailure { - Log.w(TAG, "package component failed to download from url ${info.url}, " + - "to be saved as `${info.componentName}`", it) - }.getOrNull() - } - }.awaitAll().associate { it } -} diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 94063c1c64..91b620eb44 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.android.vending.buildRequestHeaders -import com.android.vending.installer.installPackages +import com.android.vending.installer.installPackagesFromNetwork import com.android.vending.installer.uninstallPackage import kotlinx.coroutines.runBlocking import org.microg.gms.common.DeviceConfiguration @@ -39,7 +39,6 @@ import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo import org.microg.vending.billing.proto.GoogleApiResponse -import org.microg.vending.delivery.downloadPackageComponents import org.microg.vending.enterprise.EnterpriseApp import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.enterprise.AppState @@ -104,22 +103,17 @@ class VendingActivity : ComponentActivity() { return@runBlocking } - val packageFiles = client.downloadPackageComponents(this@VendingActivity, downloadUrls.getOrThrow(), Unit) - if (packageFiles.values.any { it == null }) { - Log.w(TAG, "Cannot proceed to installation as not all files were downloaded") - apps[app] = previousState - return@runBlocking - } - runCatching { - installPackages( - app.packageName, - packageFiles.values.filterNotNull(), - isUpdate + installPackagesFromNetwork( + packageName = app.packageName, + components = downloadUrls.getOrThrow(), + httpClient = client, + isUpdate = isUpdate ) }.onSuccess { apps[app] = AppState.INSTALLED }.onFailure { + Log.w(TAG, "Installation from network unsuccessful.") apps[app] = previousState } } diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index f61a09f90c..db62562fe1 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -10,30 +10,65 @@ import android.content.pm.PackageInstaller.SessionParams import android.os.Build import android.util.Log import androidx.annotation.RequiresApi +import com.google.android.finsky.splitinstallservice.PackageComponent import kotlinx.coroutines.CompletableDeferred +import org.microg.vending.billing.core.HttpClient import java.io.File import java.io.FileInputStream import java.io.IOException +import java.io.OutputStream @RequiresApi(Build.VERSION_CODES.LOLLIPOP) internal suspend fun Context.installPackages( - callingPackage: String, + packageName: String, componentFiles: List, isUpdate: Boolean = false +) = installPackagesInternal( + packageName = packageName, + componentNames = componentFiles.map { it.name }, + isUpdate = isUpdate +) { fileName, to -> + val component = componentFiles.find { it.name == fileName }!! + FileInputStream(component).use { it.copyTo(to) } + component.delete() +} + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +internal suspend fun Context.installPackagesFromNetwork( + packageName: String, + components: List, + httpClient: HttpClient = HttpClient(), + isUpdate: Boolean = false +) = installPackagesInternal( + packageName = packageName, + componentNames = components.map { it.componentName }, + isUpdate = isUpdate +) { fileName, to -> + val component = components.find { it.componentName == fileName }!! + Log.v(TAG, "installing $fileName for $packageName from network") + httpClient.download(component.url, to) +} + +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +private suspend fun Context.installPackagesInternal( + packageName: String, + componentNames: List, + isUpdate: Boolean = false, + writeComponent: suspend (componentName: String, to: OutputStream) -> Unit ) { Log.v(TAG, "installPackages start") val packageInstaller = packageManager.packageInstaller val installed = packageManager.getInstalledPackages(0).any { - it.applicationInfo.packageName == callingPackage + it.applicationInfo.packageName == packageName } // Contrary to docs, MODE_INHERIT_EXISTING cannot be used if package is not yet installed. val params = SessionParams( if (!installed || isUpdate) SessionParams.MODE_FULL_INSTALL else SessionParams.MODE_INHERIT_EXISTING ) - params.setAppPackageName(callingPackage) - params.setAppLabel(callingPackage + "Subcontracting") + params.setAppPackageName(packageName) + params.setAppLabel(packageName + "Subcontracting") params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) try { @SuppressLint("PrivateApi") val method = SessionParams::class.java.getDeclaredMethod( @@ -45,17 +80,14 @@ internal suspend fun Context.installPackages( } val sessionId: Int var session: PackageInstaller.Session? = null - var totalDownloaded = 0L try { sessionId = packageInstaller.createSession(params) session = packageInstaller.openSession(sessionId) - componentFiles.forEach { file -> - session.openWrite(file.name, 0, -1).use { outputStream -> - FileInputStream(file).use { inputStream -> inputStream.copyTo(outputStream) } + componentNames.forEach { component -> + session.openWrite(component, 0, -1).use { outputStream -> + writeComponent(component, outputStream) session.fsync(outputStream) } - totalDownloaded += file.length() - file.delete() } val deferred = CompletableDeferred() 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 60e98f47c3..f939a02227 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 @@ -18,13 +18,12 @@ import androidx.core.app.NotificationManagerCompat import androidx.core.content.pm.PackageInfoCompat import com.android.vending.R import com.android.vending.installer.KEY_BYTES_DOWNLOADED -import com.android.vending.installer.installPackages +import com.android.vending.installer.installPackagesFromNetwork import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.HttpClient -import org.microg.vending.delivery.downloadPackageComponents import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG @@ -92,29 +91,22 @@ class SplitInstallManager(val context: Context) { splitInstallRecord[it] = DownloadStatus.PENDING } - val packageFiles = httpClient.downloadPackageComponents(context, components, SPLIT_LANGUAGE_TAG) - - packageFiles.forEach { (component, downloadFile) -> - splitInstallRecord[component] = if (downloadFile != null) DownloadStatus.COMPLETE else DownloadStatus.FAILED - } - - val installFiles = packageFiles.map { - if (it.value == null) { - Log.w(TAG, "splitInstallFlow download failed, as ${it.key} was not downloaded") - throw RuntimeException("installSplitPackage downloadSplitPackage has error") - } else it.value!! - } - Log.v(TAG, "splitInstallFlow downloaded success, downloaded ${installFiles.size} files") - val success = runCatching { - context.installPackages(callingPackage, installFiles, false) + context.installPackagesFromNetwork( + packageName = callingPackage, + components = components, + httpClient = httpClient, + isUpdate = false + ) }.isSuccess NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return if (success) { sendCompleteBroad(context, callingPackage, components.sumOf { it.size.toLong() }) + components.forEach { splitInstallRecord[it] = DownloadStatus.COMPLETE } true } else { + components.forEach { splitInstallRecord[it] = DownloadStatus.FAILED } false } } From 9ee71fb287f261db6770e43742bf41cbe884dbd4 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 26 Sep 2024 12:35:04 +0200 Subject: [PATCH 48/59] Add progress bar to work app store --- .../microg/vending/billing/core/HttpClient.kt | 24 ++++++++- .../org/microg/vending/delivery/Delivery.kt | 6 +-- .../org/microg/vending/enterprise/AppState.kt | 48 +++++++++--------- .../vending/enterprise/InstallProgress.kt | 13 +++++ .../org/microg/vending/ui/VendingActivity.kt | 40 +++++++++------ .../microg/vending/ui/components/AppRow.kt | 40 ++++++++++----- .../vending/ui/components/EnterpriseList.kt | 11 +++-- .../com/android/vending/installer/Install.kt | 49 ++++++++++++++----- .../splitinstallservice/PackageComponent.kt | 2 +- 9 files changed, 164 insertions(+), 69 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/enterprise/InstallProgress.kt 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 a2a1d11224..928e0cfa85 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 @@ -21,6 +21,7 @@ import io.ktor.http.URLBuilder import io.ktor.http.Url import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.jvm.javaio.copyTo +import io.ktor.utils.io.pool.ByteArrayPool import org.json.JSONObject import org.microg.gms.utils.singleInstanceOf import java.io.File @@ -54,11 +55,30 @@ class HttpClient { suspend fun download( url: String, downloadTo: OutputStream, - params: Map = emptyMap() + params: Map = emptyMap(), + emitProgress: (bytesDownloaded: Long) -> Unit = {} ) { client.prepareGet(url.asUrl(params)).execute { response -> val body: ByteReadChannel = response.body() - body.copyTo(out = downloadTo) + + // Modified version of `ByteReadChannel.copyTo(OutputStream, Long)` to indicate progress + val buffer = ByteArrayPool.borrow() + try { + var copied = 0L + val bufferSize = buffer.size + + do { + val rc = body.readAvailable(buffer, 0, bufferSize) + copied += rc + if (rc > 0) { + downloadTo.write(buffer, 0, rc) + } + emitProgress(copied) + } while (rc > 0) + } finally { + ByteArrayPool.recycle(buffer) + } + // don't close `downloadTo` yet } } diff --git a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt index 97eddb5709..a694045125 100644 --- a/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt +++ b/vending-app/src/main/java/org/microg/vending/delivery/Delivery.kt @@ -62,7 +62,7 @@ suspend fun HttpClient.requestDownloadUrls( val basePackage = response.payload!!.deliveryResponse!!.deliveryData?.let { if (it.baseUrl != null && it.baseBytes != null) { - PackageComponent(packageName, "base", it.baseUrl, it.baseBytes) + PackageComponent(packageName, "base", it.baseUrl, it.baseBytes.toLong()) } else null } val splitComponents = response.payload.deliveryResponse!!.deliveryData!!.splitPackages.filter { @@ -73,11 +73,11 @@ suspend fun HttpClient.requestDownloadUrls( requestSplitPackages.firstOrNull { requestComponent -> requestComponent.contains(it.splitPackageName!!) }?.let { requestComponent -> - PackageComponent(packageName, requestComponent, it.downloadUrl!!, it.size!!) + PackageComponent(packageName, requestComponent, it.downloadUrl!!, it.size!!.toLong()) } } else { // Download all offered components (server chooses) - PackageComponent(packageName, it.splitPackageName!!, it.downloadUrl!!, it.size!!) + PackageComponent(packageName, it.splitPackageName!!, it.downloadUrl!!, it.size!!.toLong()) } } diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt b/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt index 339f82f3dd..7af3f28d20 100644 --- a/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt +++ b/vending-app/src/main/java/org/microg/vending/enterprise/AppState.kt @@ -1,24 +1,28 @@ package org.microg.vending.enterprise -enum class AppState { - /** - * App cannot be installed on this user's device - */ - NOT_COMPATIBLE, - /** - * App is available, but not installed on the user's device. - */ - NOT_INSTALLED, - /** - * App is already installed on the device, but an update is available. - */ - UPDATE_AVAILABLE, - /** - * An app operation is currently outstanding - */ - PENDING, - /** - * App is installed on device and up to date. - */ - INSTALLED -} \ No newline at end of file +internal sealed interface AppState + +/** + * App cannot be installed on this user's device + */ +internal data object NotCompatible : AppState + +/** + * App is available, but not installed on the user's device. + */ +internal data object NotInstalled : AppState + +/** + * App is already installed on the device, but an update is available. + */ +internal data object UpdateAvailable : AppState + +/** + * An unspecific app operation is currently outstanding + */ +internal data object Pending : AppState + +/** + * App is installed on device and up to date. + */ +internal data object Installed : AppState \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/enterprise/InstallProgress.kt b/vending-app/src/main/java/org/microg/vending/enterprise/InstallProgress.kt new file mode 100644 index 0000000000..6967288b3d --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/enterprise/InstallProgress.kt @@ -0,0 +1,13 @@ +package org.microg.vending.enterprise + +internal sealed interface InstallProgress + +internal data class Downloading( + val bytesDownloaded: Long, + val bytesTotal: Long +) : InstallProgress, AppState +internal data object CommitingSession : InstallProgress +internal data object InstallComplete : InstallProgress +internal data class InstallError( + val errorMessage: String +) : InstallProgress \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 91b620eb44..7b0062d489 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -42,6 +42,13 @@ import org.microg.vending.billing.proto.GoogleApiResponse import org.microg.vending.enterprise.EnterpriseApp import org.microg.vending.delivery.requestDownloadUrls import org.microg.vending.enterprise.AppState +import org.microg.vending.enterprise.CommitingSession +import org.microg.vending.enterprise.Downloading +import org.microg.vending.enterprise.Installed +import org.microg.vending.enterprise.NotCompatible +import org.microg.vending.enterprise.NotInstalled +import org.microg.vending.enterprise.Pending +import org.microg.vending.enterprise.UpdateAvailable import org.microg.vending.proto.AppMeta import org.microg.vending.proto.GetItemsRequest import org.microg.vending.proto.RequestApp @@ -53,10 +60,10 @@ import java.io.IOException @RequiresApi(android.os.Build.VERSION_CODES.LOLLIPOP) class VendingActivity : ComponentActivity() { - var apps: MutableMap = mutableStateMapOf() - var networkState by mutableStateOf(NetworkState.ACTIVE) + private var apps: MutableMap = mutableStateMapOf() + private var networkState by mutableStateOf(NetworkState.ACTIVE) - var auth: AuthData? = null + private var auth: AuthData? = null override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -85,7 +92,7 @@ class VendingActivity : ComponentActivity() { runBlocking { val previousState = apps[app]!! - apps[app] = AppState.PENDING + apps[app] = Pending val client = HttpClient() @@ -109,11 +116,14 @@ class VendingActivity : ComponentActivity() { components = downloadUrls.getOrThrow(), httpClient = client, isUpdate = isUpdate - ) + ) { progress -> + if (progress is Downloading) apps[app] = progress + else if (progress is CommitingSession) apps[app] = Pending + } }.onSuccess { - apps[app] = AppState.INSTALLED - }.onFailure { - Log.w(TAG, "Installation from network unsuccessful.") + apps[app] = Installed + }.onFailure { exception -> + Log.w(TAG, "Installation from network unsuccessful.", exception) apps[app] = previousState } } @@ -125,9 +135,9 @@ class VendingActivity : ComponentActivity() { runBlocking { val previousState = apps[app]!! - apps[app] = AppState.PENDING + apps[app] = Pending runCatching { uninstallPackage(app.packageName) }.onSuccess { - apps[app] = AppState.NOT_INSTALLED + apps[app] = NotInstalled }.onFailure { apps[app] = previousState } @@ -210,11 +220,11 @@ class VendingActivity : ComponentActivity() { item.offer!!.version!!.versionCode!! } else null - val state = if (!available && installedDetails == null) AppState.NOT_COMPATIBLE - else if (!available && installedDetails != null) AppState.INSTALLED - else if (available && installedDetails == null) AppState.NOT_INSTALLED - else if (available && installedDetails != null && installedDetails.versionCode < versionCode!!) AppState.UPDATE_AVAILABLE - else /* if (available && installedDetails != null) */ AppState.INSTALLED + val state = if (!available && installedDetails == null) NotCompatible + else if (!available && installedDetails != null) Installed + else if (available && installedDetails == null) NotInstalled + else if (available && installedDetails != null && installedDetails.versionCode < versionCode!!) UpdateAvailable + else /* if (available && installedDetails != null) */ Installed EnterpriseApp( packageName, diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt index b49c36e05b..7242bda68b 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/AppRow.kt @@ -26,9 +26,15 @@ import coil.compose.AsyncImage import com.android.vending.R import org.microg.vending.enterprise.App import org.microg.vending.enterprise.AppState +import org.microg.vending.enterprise.Downloading +import org.microg.vending.enterprise.Installed +import org.microg.vending.enterprise.NotCompatible +import org.microg.vending.enterprise.NotInstalled +import org.microg.vending.enterprise.Pending +import org.microg.vending.enterprise.UpdateAvailable @Composable -fun AppRow(app: App, state: AppState, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) { +internal fun AppRow(app: App, state: AppState, install: () -> Unit, update: () -> Unit, uninstall: () -> Unit) { Row( Modifier.padding(top = 8.dp, bottom = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.Start), @@ -47,28 +53,34 @@ fun AppRow(app: App, state: AppState, install: () -> Unit, update: () -> Unit, u Text(app.displayName) Spacer(Modifier.weight(1f)) - if (state == AppState.NOT_COMPATIBLE) { + if (state == NotCompatible) { Icon(Icons.Default.Warning, null, Modifier.padding(end=8.dp), tint = MaterialTheme.colorScheme.secondary) // TODO better UI } - if (state == AppState.UPDATE_AVAILABLE || state == AppState.INSTALLED) { + if (state == UpdateAvailable || state == Installed) { IconButton(uninstall) { Icon(Icons.Default.Delete, stringResource(R.string.vending_overview_row_action_uninstall), tint = MaterialTheme.colorScheme.secondary) } } - if (state == AppState.UPDATE_AVAILABLE) { + if (state == UpdateAvailable) { FilledIconButton(update, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { Icon(painterResource(R.drawable.ic_update), stringResource(R.string.vending_overview_row_action_update), tint = MaterialTheme.colorScheme.secondary) } } - if (state == AppState.NOT_INSTALLED) { + if (state == NotInstalled) { FilledIconButton(install, colors = IconButtonDefaults.filledIconButtonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)) { Icon(painterResource(R.drawable.ic_download), stringResource(R.string.vending_overview_row_action_install), tint = MaterialTheme.colorScheme.secondary) } } - if (state == AppState.PENDING) { + if (state == Pending) { CircularProgressIndicator(Modifier.padding(4.dp)) } + if (state is Downloading) { + CircularProgressIndicator( + progress = state.bytesDownloaded.toFloat() / state.bytesTotal.toFloat(), + modifier = Modifier.padding(4.dp) + ) + } } } @@ -77,29 +89,35 @@ private val previewApp = App("org.mozilla.firefox", 0, "Firefox", null, null) @Preview @Composable fun AppRowNotCompatiblePreview() { - AppRow(previewApp, AppState.NOT_COMPATIBLE, {}, {}, {}) + AppRow(previewApp, NotCompatible, {}, {}, {}) } @Preview @Composable fun AppRowNotInstalledPreview() { - AppRow(previewApp, AppState.NOT_INSTALLED, {}, {}, {}) + AppRow(previewApp, NotInstalled, {}, {}, {}) } @Preview @Composable fun AppRowUpdateablePreview() { - AppRow(previewApp, AppState.UPDATE_AVAILABLE, {}, {}, {}) + AppRow(previewApp, UpdateAvailable, {}, {}, {}) } @Preview @Composable fun AppRowInstalledPreview() { - AppRow(previewApp, AppState.INSTALLED, {}, {}, {}) + AppRow(previewApp, Installed, {}, {}, {}) } @Preview @Composable fun AppRowPendingPreview() { - AppRow(previewApp, AppState.PENDING, {}, {}, {}) + AppRow(previewApp, Pending, {}, {}, {}) } + +@Preview +@Composable +fun AppRowProgressPreview() { + AppRow(previewApp, Downloading(75, 100), {}, {}, {}) +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index bedec8d59c..84e217bf80 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -26,11 +26,14 @@ import androidx.compose.ui.unit.dp import com.android.vending.R import org.microg.vending.enterprise.AppState import org.microg.vending.enterprise.EnterpriseApp +import org.microg.vending.enterprise.Installed +import org.microg.vending.enterprise.NotCompatible +import org.microg.vending.enterprise.NotInstalled import org.microg.vending.enterprise.proto.AppInstallPolicy @Composable -fun EnterpriseList(appStates: Map, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { +internal fun EnterpriseList(appStates: Map, install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit, uninstall: (app: EnterpriseApp) -> Unit) { if (appStates.isNotEmpty()) LazyColumn(Modifier.padding(horizontal = 16.dp)) { val apps = appStates.keys @@ -105,9 +108,9 @@ fun InListWarning(@StringRes text: Int) { fun EnterpriseListPreview() { EnterpriseList( mapOf( - EnterpriseApp("com.android.vending", 0, "Market", null, "", AppInstallPolicy.MANDATORY) to AppState.INSTALLED, - EnterpriseApp("org.mozilla.firefox", 0, "Firefox", null, "", AppInstallPolicy.OPTIONAL) to AppState.NOT_INSTALLED, - EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", null, "", AppInstallPolicy.OPTIONAL) to AppState.NOT_COMPATIBLE + EnterpriseApp("com.android.vending", 0, "Market", null, "", AppInstallPolicy.MANDATORY) to Installed, + EnterpriseApp("org.mozilla.firefox", 0, "Firefox", null, "", AppInstallPolicy.OPTIONAL) to NotInstalled, + EnterpriseApp("org.thoughtcrime.securesms", 0, "Signal", null, "", AppInstallPolicy.OPTIONAL) to NotCompatible ), { _, _ -> }, {} ) } diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index db62562fe1..5b5e685e11 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -13,6 +13,11 @@ import androidx.annotation.RequiresApi import com.google.android.finsky.splitinstallservice.PackageComponent import kotlinx.coroutines.CompletableDeferred import org.microg.vending.billing.core.HttpClient +import org.microg.vending.enterprise.CommitingSession +import org.microg.vending.enterprise.Downloading +import org.microg.vending.enterprise.InstallComplete +import org.microg.vending.enterprise.InstallError +import org.microg.vending.enterprise.InstallProgress import java.io.File import java.io.FileInputStream import java.io.IOException @@ -38,15 +43,28 @@ internal suspend fun Context.installPackagesFromNetwork( packageName: String, components: List, httpClient: HttpClient = HttpClient(), - isUpdate: Boolean = false -) = installPackagesInternal( - packageName = packageName, - componentNames = components.map { it.componentName }, - isUpdate = isUpdate -) { fileName, to -> - val component = components.find { it.componentName == fileName }!! - Log.v(TAG, "installing $fileName for $packageName from network") - httpClient.download(component.url, to) + isUpdate: Boolean = false, + emitProgress: (InstallProgress) -> Unit = {} +) { + + val downloadProgress = mutableMapOf() + + installPackagesInternal( + packageName = packageName, + componentNames = components.map { it.componentName }, + isUpdate = isUpdate, + emitProgress = emitProgress, + ) { fileName, to -> + val component = components.find { it.componentName == fileName }!! + Log.v(TAG, "installing $fileName for $packageName from network") + httpClient.download(component.url, to) { progress -> + downloadProgress[component] = progress + emitProgress(Downloading( + bytesDownloaded = downloadProgress.values.sum(), + bytesTotal = components.sumOf { it.size } + )) + } + } } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) @@ -54,6 +72,7 @@ private suspend fun Context.installPackagesInternal( packageName: String, componentNames: List, isUpdate: Boolean = false, + emitProgress: (InstallProgress) -> Unit = {}, writeComponent: suspend (componentName: String, to: OutputStream) -> Unit ) { Log.v(TAG, "installPackages start") @@ -92,13 +111,21 @@ private suspend fun Context.installPackagesInternal( val deferred = CompletableDeferred() SessionResultReceiver.pendingSessions[sessionId] = SessionResultReceiver.OnResult( - onSuccess = { deferred.complete(Unit) }, - onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) } + onSuccess = { + deferred.complete(Unit) + emitProgress(InstallComplete) + }, + onFailure = { message -> + deferred.completeExceptionally(RuntimeException(message)) + emitProgress(InstallError(message ?: "UNKNOWN")) + } ) val intent = Intent(this, SessionResultReceiver::class.java) val pendingIntent = PendingIntent.getBroadcast(this, sessionId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) + + emitProgress(CommitingSession) session.commit(pendingIntent.intentSender) Log.d(TAG, "installPackages session commit") diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt index 5d06d44fd5..1939af29db 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/PackageComponent.kt @@ -7,5 +7,5 @@ data class PackageComponent( /** * Size in bytes */ - val size: Int + val size: Long ) \ No newline at end of file From 352260d0888c761b71b0b36278c936ec8b86728c Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 26 Sep 2024 12:59:09 +0200 Subject: [PATCH 49/59] Reimplement caching in `HttpClient` --- .../microg/vending/billing/core/HttpClient.kt | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt b/vending-app/src/main/java/org/microg/vending/billing/core/HttpClient.kt index 928e0cfa85..86fe2408a3 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 @@ -6,6 +6,7 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.cache.HttpCache import io.ktor.client.plugins.timeout import io.ktor.client.request.forms.submitForm import io.ktor.client.request.get @@ -20,7 +21,6 @@ import io.ktor.http.ParametersImpl import io.ktor.http.URLBuilder import io.ktor.http.Url import io.ktor.utils.io.ByteReadChannel -import io.ktor.utils.io.jvm.javaio.copyTo import io.ktor.utils.io.pool.ByteArrayPool import org.json.JSONObject import org.microg.gms.utils.singleInstanceOf @@ -35,7 +35,12 @@ class HttpClient { private val client = singleInstanceOf { HttpClient(OkHttp) { expectSuccess = true - //install(HttpCache) + install(HttpTimeout) + } } + + private val clientWithCache = singleInstanceOf { HttpClient(OkHttp) { + expectSuccess = true + install(HttpCache) install(HttpTimeout) } } @@ -87,9 +92,10 @@ class HttpClient { headers: Map = emptyMap(), params: Map = emptyMap(), adapter: ProtoAdapter, + cache: Boolean = true ): O { - val response = client.get(url.asUrl(params)) { + val response = (if (cache) clientWithCache else client).get(url.asUrl(params)) { headers { headers.forEach { append(it.key, it.value) @@ -108,9 +114,9 @@ class HttpClient { headers: Map = emptyMap(), params: Map = emptyMap(), adapter: ProtoAdapter, - cache: Boolean = false // TODO not implemented + cache: Boolean = false ): O { - val response = client.post(url.asUrl(params)) { + val response = (if (cache) clientWithCache else client).post(url.asUrl(params)) { setBody(ByteArray(0)) headers { headers.forEach { @@ -135,9 +141,9 @@ class HttpClient { params: Map = emptyMap(), payload: I, adapter: ProtoAdapter, - cache: Boolean = false // TODO not implemented + cache: Boolean = false ): O { - val response = client.post(url.asUrl(params)) { + val response = (if (cache) clientWithCache else client).post(url.asUrl(params)) { setBody(ByteReadChannel(payload.encode())) headers { headers.forEach { @@ -161,9 +167,9 @@ class HttpClient { headers: Map = emptyMap(), params: Map = emptyMap(), payload: JSONObject, - cache: Boolean = false // TODO not implemented + cache: Boolean = false ): JSONObject { - val response = client.post(url.asUrl(params)) { + val response = (if (cache) clientWithCache else client).post(url.asUrl(params)) { setBody(payload.toString()) headers { headers.forEach { @@ -188,9 +194,9 @@ class HttpClient { params: Map = emptyMap(), form: Map = emptyMap(), adapter: ProtoAdapter, - cache: Boolean = false // TODO not implemented + cache: Boolean = false ): O { - val response = client.submitForm( + val response = (if (cache) clientWithCache else client).submitForm( formParameters = ParametersImpl(form.mapValues { listOf(it.key) }), encodeInQuery = false ) { From 2109e46d994cd020b0c45427b93a46a170d1ea43 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Thu, 26 Sep 2024 13:08:44 +0200 Subject: [PATCH 50/59] Discard session after exception to clear up storage Storage leaks can still happen if session commits have errors (for instance, because the device was shutdown during installation), as those sessions can still be reopened and retried according to docs. However, we may expect all dangling sessions to be cleared after a certain timespan in the magnitude of a day. --- .../kotlin/com/android/vending/installer/Install.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index 5b5e685e11..6b270df1be 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -102,10 +102,10 @@ private suspend fun Context.installPackagesInternal( try { sessionId = packageInstaller.createSession(params) session = packageInstaller.openSession(sessionId) - componentNames.forEach { component -> + for (component in componentNames) { session.openWrite(component, 0, -1).use { outputStream -> writeComponent(component, outputStream) - session.fsync(outputStream) + session!!.fsync(outputStream) } } val deferred = CompletableDeferred() @@ -127,6 +127,8 @@ private suspend fun Context.installPackagesInternal( emitProgress(CommitingSession) session.commit(pendingIntent.intentSender) + // don't abandon if `finally` step is reached after this point + session = null Log.d(TAG, "installPackages session commit") return deferred.await() @@ -134,6 +136,8 @@ private suspend fun Context.installPackagesInternal( Log.w(TAG, "Error installing packages", e) throw e } finally { - session?.close() + Log.d(TAG, "Discarding session after error") + // discard downloaded data + session?.abandon() } } From c237146c9c0c8949d9a8ceac115039478355a853 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Tue, 1 Oct 2024 21:04:34 +0200 Subject: [PATCH 51/59] Notifications upon install progress --- .../microg/vending/billing/core/HttpClient.kt | 2 +- .../vending/ui/InstallProgressNotification.kt | 105 ++++++++++++++++++ .../org/microg/vending/ui/VendingActivity.kt | 12 +- .../com/android/vending/installer/Install.kt | 28 ++--- .../SplitInstallManager.kt | 56 ++-------- .../src/main/res/values-zh-rCN/strings.xml | 2 +- vending-app/src/main/res/values/strings.xml | 10 +- 7 files changed, 154 insertions(+), 61 deletions(-) create mode 100644 vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt 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 86fe2408a3..75cbe6d094 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 @@ -77,8 +77,8 @@ class HttpClient { copied += rc if (rc > 0) { downloadTo.write(buffer, 0, rc) + emitProgress(copied) } - emitProgress(copied) } while (rc > 0) } finally { ByteArrayPool.recycle(buffer) diff --git a/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt b/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt new file mode 100644 index 0000000000..8302158a48 --- /dev/null +++ b/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt @@ -0,0 +1,105 @@ +package org.microg.vending.ui + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager.NameNotFoundException +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.android.vending.R +import org.microg.gms.ui.TAG +import org.microg.vending.enterprise.CommitingSession +import org.microg.vending.enterprise.Downloading +import org.microg.vending.enterprise.InstallComplete +import org.microg.vending.enterprise.InstallError +import org.microg.vending.enterprise.InstallProgress + +private const val INSTALL_NOTIFICATION_CHANNEL_ID = "packageInstall" + +internal fun Context.notifySplitInstallProgress(packageName: String, sessionId: Int, progress: InstallProgress) { + + val label = try { + packageManager.getPackageInfo(packageName, 0).applicationInfo + .loadLabel(packageManager) + } catch (e: NameNotFoundException) { + Log.e(TAG, "Couldn't load label for $packageName (${e.message}). Is it not installed?") + return + } + + createNotificationChannel() + + val notificationManager = NotificationManagerCompat.from(this) + + when (progress) { + is Downloading -> getDownloadNotificationBuilder().apply { + setContentTitle(getString(R.string.installer_notification_progress_splitinstall_downloading, label)) + setProgress(progress.bytesDownloaded.toInt(), progress.bytesTotal.toInt(), false) + } + CommitingSession -> getDownloadNotificationBuilder().apply { + setContentTitle(getString(R.string.installer_notification_progress_splitinstall_commiting, label)) + setProgress(0, 1, true) + } + else -> null.also { notificationManager.cancel(sessionId) } + }?.apply { + setOngoing(true) + + notificationManager.notify(sessionId, this.build()) + } + +} + +internal fun Context.notifyInstallProgress(displayName: String, sessionId: Int, progress: InstallProgress) { + + createNotificationChannel() + getDownloadNotificationBuilder().apply { + when (progress) { + is Downloading -> { + setContentTitle(getString(R.string.installer_notification_progress_downloading, displayName)) + setProgress(progress.bytesTotal.toInt(), progress.bytesDownloaded.toInt(), false) + setOngoing(true) + } + CommitingSession -> { + setContentTitle(getString(R.string.installer_notification_progress_commiting, displayName)) + setProgress(0, 0, true) + setOngoing(true) + } + InstallComplete -> { + setContentTitle(getString(R.string.installer_notification_progress_complete, displayName)) + setSmallIcon(android.R.drawable.stat_sys_download_done) + } + is InstallError -> { + setContentTitle(getString(R.string.installer_notification_progress_failed, displayName)) + setSmallIcon(android.R.drawable.stat_notify_error) + } + } + + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.notify(sessionId, this.build()) + } + +} + +private fun Context.getDownloadNotificationBuilder() = + NotificationCompat.Builder(this, INSTALL_NOTIFICATION_CHANNEL_ID) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setLocalOnly(true) + +private fun Context.createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel( + NotificationChannel( + INSTALL_NOTIFICATION_CHANNEL_ID, + getString(R.string.installer_notification_channel_description), + NotificationManager.IMPORTANCE_LOW + ).apply { + enableVibration(false) + enableLights(false) + setShowBadge(false) + } + ) + } +} \ No newline at end of file diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 7b0062d489..68ae397c0a 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -111,12 +111,22 @@ class VendingActivity : ComponentActivity() { } runCatching { + + var lastNotification = 0L installPackagesFromNetwork( packageName = app.packageName, components = downloadUrls.getOrThrow(), httpClient = client, isUpdate = isUpdate - ) { progress -> + ) { session, progress -> + + + // Android rate limits notification updates by some vague rule of "not too many in less than one second" + if (progress !is Downloading || lastNotification + 250 < System.currentTimeMillis()) { + notifyInstallProgress(app.displayName, session, progress) + lastNotification = System.currentTimeMillis() + } + if (progress is Downloading) apps[app] = progress else if (progress is CommitingSession) apps[app] = Pending } diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index 6b270df1be..27446ff9a1 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -32,7 +32,7 @@ internal suspend fun Context.installPackages( packageName = packageName, componentNames = componentFiles.map { it.name }, isUpdate = isUpdate -) { fileName, to -> +) { session, fileName, to -> val component = componentFiles.find { it.name == fileName }!! FileInputStream(component).use { it.copyTo(to) } component.delete() @@ -44,7 +44,7 @@ internal suspend fun Context.installPackagesFromNetwork( components: List, httpClient: HttpClient = HttpClient(), isUpdate: Boolean = false, - emitProgress: (InstallProgress) -> Unit = {} + emitProgress: (session: Int, InstallProgress) -> Unit = { _, _ -> } ) { val downloadProgress = mutableMapOf() @@ -54,12 +54,12 @@ internal suspend fun Context.installPackagesFromNetwork( componentNames = components.map { it.componentName }, isUpdate = isUpdate, emitProgress = emitProgress, - ) { fileName, to -> + ) { session, fileName, to -> val component = components.find { it.componentName == fileName }!! Log.v(TAG, "installing $fileName for $packageName from network") httpClient.download(component.url, to) { progress -> downloadProgress[component] = progress - emitProgress(Downloading( + emitProgress(session, Downloading( bytesDownloaded = downloadProgress.values.sum(), bytesTotal = components.sumOf { it.size } )) @@ -72,8 +72,8 @@ private suspend fun Context.installPackagesInternal( packageName: String, componentNames: List, isUpdate: Boolean = false, - emitProgress: (InstallProgress) -> Unit = {}, - writeComponent: suspend (componentName: String, to: OutputStream) -> Unit + emitProgress: (session: Int, InstallProgress) -> Unit = { _, _ -> }, + writeComponent: suspend (session: Int, componentName: String, to: OutputStream) -> Unit ) { Log.v(TAG, "installPackages start") @@ -87,7 +87,7 @@ private suspend fun Context.installPackagesInternal( else SessionParams.MODE_INHERIT_EXISTING ) params.setAppPackageName(packageName) - params.setAppLabel(packageName + "Subcontracting") + params.setAppLabel(packageName) params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) try { @SuppressLint("PrivateApi") val method = SessionParams::class.java.getDeclaredMethod( @@ -104,7 +104,7 @@ private suspend fun Context.installPackagesInternal( session = packageInstaller.openSession(sessionId) for (component in componentNames) { session.openWrite(component, 0, -1).use { outputStream -> - writeComponent(component, outputStream) + writeComponent(sessionId, component, outputStream) session!!.fsync(outputStream) } } @@ -113,11 +113,11 @@ private suspend fun Context.installPackagesInternal( SessionResultReceiver.pendingSessions[sessionId] = SessionResultReceiver.OnResult( onSuccess = { deferred.complete(Unit) - emitProgress(InstallComplete) + emitProgress(sessionId, InstallComplete) }, onFailure = { message -> deferred.completeExceptionally(RuntimeException(message)) - emitProgress(InstallError(message ?: "UNKNOWN")) + emitProgress(sessionId, InstallError(message ?: "UNKNOWN")) } ) @@ -125,7 +125,7 @@ private suspend fun Context.installPackagesInternal( val pendingIntent = PendingIntent.getBroadcast(this, sessionId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) - emitProgress(CommitingSession) + emitProgress(sessionId, CommitingSession) session.commit(pendingIntent.intentSender) // don't abandon if `finally` step is reached after this point session = null @@ -136,8 +136,10 @@ private suspend fun Context.installPackagesInternal( Log.w(TAG, "Error installing packages", e) throw e } finally { - Log.d(TAG, "Discarding session after error") // discard downloaded data - session?.abandon() + session?.let { + Log.d(TAG, "Discarding session after error") + it.abandon() + } } } 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 f939a02227..ea8b724c49 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallManager.kt @@ -4,19 +4,13 @@ */ package com.google.android.finsky.splitinstallservice -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context import android.content.Intent -import android.content.pm.PackageManager.NameNotFoundException import android.os.Build import android.os.Bundle import android.util.Log import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat import androidx.core.content.pm.PackageInfoCompat -import com.android.vending.R import com.android.vending.installer.KEY_BYTES_DOWNLOADED import com.android.vending.installer.installPackagesFromNetwork import kotlinx.coroutines.CompletableDeferred @@ -25,12 +19,10 @@ import kotlinx.coroutines.withContext import org.microg.vending.billing.AuthManager import org.microg.vending.billing.core.HttpClient import org.microg.vending.delivery.requestDownloadUrls +import org.microg.vending.enterprise.Downloading import org.microg.vending.splitinstall.SPLIT_LANGUAGE_TAG +import org.microg.vending.ui.notifySplitInstallProgress -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_MODULE_NAME = "module_name" @@ -69,7 +61,6 @@ class SplitInstallManager(val context: Context) { if (authData?.authToken.isNullOrEmpty()) return false authData!! - notify(callingPackage) val components = runCatching { httpClient.requestDownloadUrls( @@ -83,7 +74,6 @@ class SplitInstallManager(val context: Context) { }.getOrNull() Log.v(TAG, "splitInstallFlow requestDownloadUrls returned these components: $components") if (components.isNullOrEmpty()) { - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return false } @@ -92,17 +82,24 @@ class SplitInstallManager(val context: Context) { } val success = runCatching { + + var lastNotification = 0L context.installPackagesFromNetwork( packageName = callingPackage, components = components, httpClient = httpClient, isUpdate = false - ) + ) { session, progress -> + // Android rate limits notification updates by some vague rule of "not too many in less than one second" + if (progress !is Downloading || lastNotification + 250 < System.currentTimeMillis()) { + context.notifySplitInstallProgress(callingPackage, session, progress) + lastNotification = System.currentTimeMillis() + } + } }.isSuccess - NotificationManagerCompat.from(context).cancel(SPLIT_INSTALL_NOTIFY_ID) return if (success) { - sendCompleteBroad(context, callingPackage, components.sumOf { it.size.toLong() }) + sendCompleteBroad(context, callingPackage, components.sumOf { it.size }) components.forEach { splitInstallRecord[it] = DownloadStatus.COMPLETE } true } else { @@ -123,35 +120,6 @@ class SplitInstallManager(val context: Context) { } ?: true } - /** - * Tell user about the ongoing download. - * TODO: make persistent - */ - internal fun notify(installForPackage: String) { - 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) - ) - } - - val label = try { - context.packageManager.getPackageInfo(installForPackage, 0).applicationInfo - .loadLabel(context.packageManager) - } catch (e: NameNotFoundException) { - Log.e(TAG, "Couldn't load label for $installForPackage (${e.message}). Is it not installed?") - return - } - - NotificationCompat.Builder(context, NOTIFY_CHANNEL_ID).setSmallIcon(android.R.drawable.stat_sys_download) - .setContentTitle(context.getString(R.string.split_install, label)).setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setDefaults( - NotificationCompat.DEFAULT_ALL - ).build().also { - notificationManager.notify(SPLIT_INSTALL_NOTIFY_ID, it) - } - } - private fun sendCompleteBroad(context: Context, packageName: String, bytes: Long) { Log.d(TAG, "sendCompleteBroadcast: $bytes bytes") val extra = Bundle().apply { 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 f0554d99ee..01c631e37c 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 0e617924d7..a255639d2a 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -42,5 +42,13 @@ Forget password? Learn more Verify - Downloading required components for %s + + Shows app and component installation progress. + Downloading \"%s\" + Installing \"%s\" + Installed \"%s\" + Failed to install \"%s\" + + Downloading required components for %s + Installing required components for %s From f39a889f78af35872ca60e6dbeeda9547142abf6 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 19 Oct 2024 10:13:16 +0200 Subject: [PATCH 52/59] Deduplicate auth-related files Auth files are now moved to `play-services-base-core` so they can be accessed by other the workaccount module. --- .../authenticator/WorkAccountAuthenticator.kt | 4 +- .../gms/auth/workaccount/AuthRequest.java | 265 ------------------ .../java/org/microg/gms/auth/AuthRequest.java | 1 - .../org/microg/gms/auth}/AuthResponse.java | 7 +- .../kotlin/org/microg/gms/auth/AuthPrefs.kt | 0 .../org/microg/gms/checkin/LastCheckinInfo.kt | 72 +++++ .../org/microg/gms/auth/AuthResponse.java | 132 --------- .../loginservice/AccountAuthenticator.java | 4 +- .../microg/gms/checkin/CheckinService.java | 1 - .../microg/gms/gcm/PushRegisterManager.java | 2 - .../org/microg/gms/checkin/LastCheckinInfo.kt | 82 ------ .../org/microg/gms/games/GamesService.kt | 1 - 12 files changed, 79 insertions(+), 492 deletions(-) delete mode 100644 play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java rename {play-services-core => play-services-base/core}/src/main/java/org/microg/gms/auth/AuthRequest.java (99%) rename {play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount => play-services-base/core/src/main/java/org/microg/gms/auth}/AuthResponse.java (97%) rename {play-services-core => play-services-base/core}/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt (100%) create mode 100644 play-services-base/core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt delete mode 100644 play-services-core/src/main/java/org/microg/gms/auth/AuthResponse.java delete mode 100644 play-services-core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 4708fb348d..f1b8d83127 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -15,9 +15,9 @@ import android.os.Build import android.os.Bundle import android.util.Log import com.google.android.gms.auth.workaccount.R -import org.microg.gms.auth.workaccount.AuthRequest -import org.microg.gms.auth.workaccount.AuthResponse import org.microg.gms.common.PackageUtils +import org.microg.gms.auth.AuthRequest +import org.microg.gms.auth.AuthResponse import java.io.IOException class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java deleted file mode 100644 index b4070f1274..0000000000 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthRequest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2023 microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ -// TODO: deduplicate file -package org.microg.gms.auth.workaccount; - -import static org.microg.gms.common.HttpFormClient.RequestContent; -import static org.microg.gms.common.HttpFormClient.RequestHeader; - -import android.content.Context; -import android.util.Log; - -import org.microg.gms.common.Constants; -import org.microg.gms.common.HttpFormClient; -import org.microg.gms.common.Utils; -import org.microg.gms.profile.Build; -import org.microg.gms.profile.ProfileManager; - -import java.io.IOException; -import java.math.BigInteger; -import java.util.Locale; -import java.util.Map; - -public class AuthRequest extends HttpFormClient.Request { - private static final String SERVICE_URL = "https://android.googleapis.com/auth"; - private static final String USER_AGENT = "GoogleAuth/1.4 (%s %s); gzip"; - - @RequestHeader("User-Agent") - private String userAgent; - - @RequestHeader("app") - @RequestContent("app") - public String app; - @RequestContent("client_sig") - public String appSignature; - @RequestContent("callerPkg") - public String caller; - @RequestContent("callerSig") - public String callerSignature; - @RequestHeader(value = "device", nullPresent = true) - @RequestContent(value = "androidId", nullPresent = true) - public String androidIdHex; - @RequestContent("sdk_version") - public int sdkVersion; - @RequestContent("device_country") - public String countryCode; - @RequestContent("operatorCountry") - public String operatorCountryCode; - @RequestContent("lang") - public String locale; - @RequestContent("google_play_services_version") - public int gmsVersion = Constants.GMS_VERSION_CODE; - @RequestContent("accountType") - public String accountType; - @RequestContent("Email") - public String email; - @RequestContent("service") - public String service; - @RequestContent("source") - public String source; - @RequestContent({"is_called_from_account_manager", "_opt_is_called_from_account_manager"}) - public boolean isCalledFromAccountManager; - @RequestContent("Token") - public String token; - @RequestContent("system_partition") - public boolean systemPartition; - @RequestContent("get_accountid") - public boolean getAccountId; - @RequestContent("ACCESS_TOKEN") - public boolean isAccessToken; - @RequestContent("droidguard_results") - public String droidguardResults; - @RequestContent("has_permission") - public boolean hasPermission; - @RequestContent("add_account") - public boolean addAccount; - @RequestContent("delegation_type") - public String delegationType; - @RequestContent("delegatee_user_id") - public String delegateeUserId; - @RequestContent("oauth2_foreground") - public String oauth2Foreground; - @RequestContent("token_request_options") - public String tokenRequestOptions; - @RequestContent("it_caveat_types") - public String itCaveatTypes; - @RequestContent("check_email") - public boolean checkEmail; - @RequestContent("request_visible_actions") - public String requestVisibleActions; - @RequestContent("oauth2_prompt") - public String oauth2Prompt; - @RequestContent("oauth2_include_profile") - public String oauth2IncludeProfile; - @RequestContent("oauth2_include_email") - public String oauth2IncludeEmail; - @HttpFormClient.RequestContentDynamic - public Map dynamicFields; - - public String deviceName; - public String buildVersion; - - @Override - protected void prepare() { - userAgent = String.format(USER_AGENT, deviceName, buildVersion); - } - - public AuthRequest build(Context context) { - ProfileManager.ensureInitialized(context); - sdkVersion = Build.VERSION.SDK_INT; - deviceName = Build.DEVICE; - buildVersion = Build.ID; - return this; - } - - public AuthRequest source(String source) { - this.source = source; - return this; - } - - public AuthRequest locale(Locale locale) { - this.locale = locale.toString(); - this.countryCode = locale.getCountry().toLowerCase(); - this.operatorCountryCode = locale.getCountry().toLowerCase(); - return this; - } - - public AuthRequest fromContext(Context context) { - build(context); - locale(Utils.getLocale(context)); - if (true) { - androidIdHex = new BigInteger(GServices.INSTANCE.getString(context.getContentResolver(), "android_id", "0")).toString(16); - Log.d("gsf", androidIdHex); - } - if (false) { - deviceName = ""; - buildVersion = ""; - } - return this; - } - - public AuthRequest email(String email) { - this.email = email; - return this; - } - - public AuthRequest token(String token) { - this.token = token; - return this; - } - - public AuthRequest service(String service) { - this.service = service; - return this; - } - - public AuthRequest app(String app, String appSignature) { - this.app = app; - this.appSignature = appSignature; - return this; - } - - public AuthRequest appIsGms() { - return app(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1); - } - - public AuthRequest callerIsGms() { - return caller(Constants.GMS_PACKAGE_NAME, Constants.GMS_PACKAGE_SIGNATURE_SHA1); - } - - public AuthRequest callerIsApp() { - return caller(app, appSignature); - } - - public AuthRequest caller(String caller, String callerSignature) { - this.caller = caller; - this.callerSignature = callerSignature; - return this; - } - - public AuthRequest calledFromAccountManager() { - isCalledFromAccountManager = true; - return this; - } - - public AuthRequest addAccount() { - addAccount = true; - return this; - } - - public AuthRequest systemPartition(boolean systemPartition) { - this.systemPartition = systemPartition; - return this; - } - - public AuthRequest hasPermission(boolean hasPermission) { - this.hasPermission = hasPermission; - return this; - } - - public AuthRequest getAccountId() { - getAccountId = true; - return this; - } - - public AuthRequest isAccessToken() { - isAccessToken = true; - return this; - } - - public AuthRequest droidguardResults(String droidguardResults) { - this.droidguardResults = droidguardResults; - return this; - } - - public AuthRequest delegation(int delegationType, String delegateeUserId) { - this.delegationType = delegationType == 0 ? null : Integer.toString(delegationType); - this.delegateeUserId = delegateeUserId; - return this; - } - - public AuthRequest oauth2Foreground(String oauth2Foreground) { - this.oauth2Foreground = oauth2Foreground; - return this; - } - - public AuthRequest tokenRequestOptions(String tokenRequestOptions) { - this.tokenRequestOptions = tokenRequestOptions; - return this; - } - - public AuthRequest oauth2IncludeProfile(String oauth2IncludeProfile) { - this.oauth2IncludeProfile = oauth2IncludeProfile; - return this; - } - - public AuthRequest oauth2IncludeEmail(String oauth2IncludeProfile) { - this.oauth2IncludeEmail = oauth2IncludeEmail; - return this; - } - - public AuthRequest oauth2Prompt(String oauth2Prompt) { - this.oauth2Prompt = oauth2Prompt; - return this; - } - - public AuthRequest itCaveatTypes(String itCaveatTypes) { - this.itCaveatTypes = itCaveatTypes; - return this; - } - - public AuthRequest putDynamicFiledMap(Map dynamicFields) { - this.dynamicFields = dynamicFields; - return this; - } - - public AuthResponse getResponse() throws IOException { - return HttpFormClient.request(SERVICE_URL, this, AuthResponse.class); - } - - public void getResponseAsync(HttpFormClient.Callback callback) { - HttpFormClient.requestAsync(SERVICE_URL, this, AuthResponse.class, callback); - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthRequest.java b/play-services-base/core/src/main/java/org/microg/gms/auth/AuthRequest.java similarity index 99% rename from play-services-core/src/main/java/org/microg/gms/auth/AuthRequest.java rename to play-services-base/core/src/main/java/org/microg/gms/auth/AuthRequest.java index 2a253d3857..fc50c5b897 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthRequest.java +++ b/play-services-base/core/src/main/java/org/microg/gms/auth/AuthRequest.java @@ -13,7 +13,6 @@ import org.microg.gms.common.HttpFormClient; import org.microg.gms.common.Utils; import org.microg.gms.profile.ProfileManager; -import org.microg.gms.settings.SettingsContract; import java.io.IOException; import java.util.Locale; diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java b/play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java similarity index 97% rename from play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java rename to play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java index d00774700d..6e945b75e6 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/AuthResponse.java +++ b/play-services-base/core/src/main/java/org/microg/gms/auth/AuthResponse.java @@ -13,15 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// TODO: deduplicate file -package org.microg.gms.auth.workaccount; -import static org.microg.gms.common.HttpFormClient.ResponseField; +package org.microg.gms.auth; import android.util.Log; import java.lang.reflect.Field; +import static org.microg.gms.common.HttpFormClient.ResponseField; + public class AuthResponse { private static final String TAG = "GmsAuthResponse"; @@ -128,6 +128,7 @@ public String toString() { if (auths != null) sb.append(", auths='").append(auths).append('\''); if (itMetadata != null) sb.append(", itMetadata='").append(itMetadata).append('\''); if (resolutionDataBase64 != null) sb.append(", resolutionDataBase64='").append(resolutionDataBase64).append('\''); + if (capabilities != null) sb.append(", capabilitites='").append(capabilities).append('\''); sb.append('}'); return sb.toString(); } diff --git a/play-services-core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt similarity index 100% rename from play-services-core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt rename to play-services-base/core/src/main/kotlin/org/microg/gms/auth/AuthPrefs.kt diff --git a/play-services-base/core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt b/play-services-base/core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt new file mode 100644 index 0000000000..d643abb091 --- /dev/null +++ b/play-services-base/core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt @@ -0,0 +1,72 @@ +package org.microg.gms.checkin + +import android.content.Context +import org.microg.gms.settings.SettingsContract + +data class LastCheckinInfo( + val lastCheckin: Long, + val androidId: Long, + val securityToken: Long, + val digest: String, + val versionInfo: String, + val deviceDataVersionInfo: String, +) { + + constructor(r: CheckinResponse) : this( + lastCheckin = r.timeMs ?: 0L, + androidId = r.androidId ?: 0L, + securityToken = r.securityToken ?: 0L, + digest = r.digest ?: SettingsContract.CheckIn.INITIAL_DIGEST, + versionInfo = r.versionInfo ?: "", + deviceDataVersionInfo = r.deviceDataVersionInfo ?: "", + ) + + companion object { + @JvmStatic + fun read(context: Context): LastCheckinInfo { + val projection = arrayOf( + SettingsContract.CheckIn.ANDROID_ID, + SettingsContract.CheckIn.DIGEST, + SettingsContract.CheckIn.LAST_CHECK_IN, + SettingsContract.CheckIn.SECURITY_TOKEN, + SettingsContract.CheckIn.VERSION_INFO, + SettingsContract.CheckIn.DEVICE_DATA_VERSION_INFO, + ) + return SettingsContract.getSettings( + context, + SettingsContract.CheckIn.getContentUri(context), + projection + ) { c -> + LastCheckinInfo( + androidId = c.getLong(0), + digest = c.getString(1), + lastCheckin = c.getLong(2), + securityToken = c.getLong(3), + versionInfo = c.getString(4), + deviceDataVersionInfo = c.getString(5), + ) + } + } + + @JvmStatic + fun clear(context: Context) = + SettingsContract.setSettings(context, SettingsContract.CheckIn.getContentUri(context)) { + put(SettingsContract.CheckIn.ANDROID_ID, 0L) + put(SettingsContract.CheckIn.DIGEST, SettingsContract.CheckIn.INITIAL_DIGEST) + put(SettingsContract.CheckIn.LAST_CHECK_IN, 0L) + put(SettingsContract.CheckIn.SECURITY_TOKEN, 0L) + put(SettingsContract.CheckIn.VERSION_INFO, "") + put(SettingsContract.CheckIn.DEVICE_DATA_VERSION_INFO, "") + } + } + + fun write(context: Context) = + SettingsContract.setSettings(context, SettingsContract.CheckIn.getContentUri(context)) { + put(SettingsContract.CheckIn.ANDROID_ID, androidId) + put(SettingsContract.CheckIn.DIGEST, digest) + put(SettingsContract.CheckIn.LAST_CHECK_IN, lastCheckin) + put(SettingsContract.CheckIn.SECURITY_TOKEN, securityToken) + put(SettingsContract.CheckIn.VERSION_INFO, versionInfo) + put(SettingsContract.CheckIn.DEVICE_DATA_VERSION_INFO, deviceDataVersionInfo) + } +} \ No newline at end of file diff --git a/play-services-core/src/main/java/org/microg/gms/auth/AuthResponse.java b/play-services-core/src/main/java/org/microg/gms/auth/AuthResponse.java deleted file mode 100644 index 8937580ff4..0000000000 --- a/play-services-core/src/main/java/org/microg/gms/auth/AuthResponse.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.microg.gms.auth; - -import android.util.Log; - -import java.lang.reflect.Field; - -import static org.microg.gms.common.HttpFormClient.ResponseField; - -public class AuthResponse { - private static final String TAG = "GmsAuthResponse"; - - @ResponseField("SID") - public String Sid; - @ResponseField("LSID") - public String LSid; - @ResponseField("Auth") - public String auth; - @ResponseField("Token") - public String token; - @ResponseField("Email") - public String email; - @ResponseField("services") - public String services; - @ResponseField("GooglePlusUpgrade") - public boolean isGooglePlusUpgrade; - @ResponseField("PicasaUser") - public String picasaUserName; - @ResponseField("RopText") - public String ropText; - @ResponseField("RopRevision") - public int ropRevision; - @ResponseField("firstName") - public String firstName; - @ResponseField("lastName") - public String lastName; - @ResponseField("issueAdvice") - public String issueAdvice; - @ResponseField("accountId") - public String accountId; - @ResponseField("Expiry") - public long expiry = -1; - @ResponseField("storeConsentRemotely") - public boolean storeConsentRemotely = true; - @ResponseField("Permission") - public String permission; - @ResponseField("ScopeConsentDetails") - public String scopeConsentDetails; - @ResponseField("ConsentDataBase64") - public String consentDataBase64; - @ResponseField("grantedScopes") - public String grantedScopes; - @ResponseField("itMetadata") - public String itMetadata; - @ResponseField("ResolutionDataBase64") - public String resolutionDataBase64; - @ResponseField("it") - public String auths; - - public static AuthResponse parse(String result) { - AuthResponse response = new AuthResponse(); - String[] entries = result.split("\n"); - for (String s : entries) { - String[] keyValuePair = s.split("=", 2); - String key = keyValuePair[0].trim(); - String value = keyValuePair[1].trim(); - try { - for (Field field : AuthResponse.class.getDeclaredFields()) { - if (field.isAnnotationPresent(ResponseField.class) && - key.equals(field.getAnnotation(ResponseField.class).value())) { - if (field.getType().equals(String.class)) { - field.set(response, value); - } else if (field.getType().equals(boolean.class)) { - field.setBoolean(response, value.equals("1")); - } else if (field.getType().equals(long.class)) { - field.setLong(response, Long.parseLong(value)); - } else if (field.getType().equals(int.class)) { - field.setInt(response, Integer.parseInt(value)); - } - } - } - } catch (Exception e) { - Log.w(TAG, e); - } - } - return response; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("AuthResponse{"); - sb.append("auth='").append(auth).append('\''); - if (Sid != null) sb.append(", Sid='").append(Sid).append('\''); - if (LSid != null) sb.append(", LSid='").append(LSid).append('\''); - if (token != null) sb.append(", token='").append(token).append('\''); - if (email != null) sb.append(", email='").append(email).append('\''); - if (services != null) sb.append(", services='").append(services).append('\''); - if (isGooglePlusUpgrade) sb.append(", isGooglePlusUpgrade=").append(isGooglePlusUpgrade); - if (picasaUserName != null) sb.append(", picasaUserName='").append(picasaUserName).append('\''); - if (ropText != null) sb.append(", ropText='").append(ropText).append('\''); - if (ropRevision != 0) sb.append(", ropRevision=").append(ropRevision); - if (firstName != null) sb.append(", firstName='").append(firstName).append('\''); - if (lastName != null) sb.append(", lastName='").append(lastName).append('\''); - if (issueAdvice != null) sb.append(", issueAdvice='").append(issueAdvice).append('\''); - if (accountId != null) sb.append(", accountId='").append(accountId).append('\''); - if (expiry != -1) sb.append(", expiry=").append(expiry); - if (!storeConsentRemotely) sb.append(", storeConsentRemotely=").append(storeConsentRemotely); - if (permission != null) sb.append(", permission='").append(permission).append('\''); - if (scopeConsentDetails != null) sb.append(", scopeConsentDetails='").append(scopeConsentDetails).append('\''); - if (consentDataBase64 != null) sb.append(", consentDataBase64='").append(consentDataBase64).append('\''); - if (auths != null) sb.append(", auths='").append(auths).append('\''); - if (itMetadata != null) sb.append(", itMetadata='").append(itMetadata).append('\''); - if (resolutionDataBase64 != null) sb.append(", resolutionDataBase64='").append(resolutionDataBase64).append('\''); - sb.append('}'); - return sb.toString(); - } -} diff --git a/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java b/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java index ba3048eb7a..1d2d72f1cd 100644 --- a/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java +++ b/play-services-core/src/main/java/org/microg/gms/auth/loginservice/AccountAuthenticator.java @@ -27,13 +27,11 @@ import android.util.Base64; import android.util.Log; -import com.google.android.gms.R; - import com.google.android.gms.common.internal.CertData; import org.microg.gms.auth.*; import org.microg.gms.auth.login.LoginActivity; import org.microg.gms.common.PackageUtils; -import org.microg.gms.utils.ExtendedPackageInfo; +import org.microg.gms.auth.AuthResponse; import org.microg.gms.utils.PackageManagerUtilsKt; import java.util.Arrays; diff --git a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java index a349fa3c88..44c503f156 100644 --- a/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java +++ b/play-services-core/src/main/java/org/microg/gms/checkin/CheckinService.java @@ -33,7 +33,6 @@ import androidx.core.app.PendingIntentCompat; import androidx.legacy.content.WakefulBroadcastReceiver; -import com.google.android.gms.R; import com.google.android.gms.checkin.internal.ICheckinService; import org.microg.gms.auth.AuthConstants; diff --git a/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java b/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java index cfa19c7187..5f805e8003 100644 --- a/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java +++ b/play-services-core/src/main/java/org/microg/gms/gcm/PushRegisterManager.java @@ -22,8 +22,6 @@ import org.microg.gms.checkin.LastCheckinInfo; import org.microg.gms.common.HttpFormClient; -import org.microg.gms.common.PackageUtils; -import org.microg.gms.common.Utils; import org.microg.gms.utils.ExtendedPackageInfo; import java.io.IOException; diff --git a/play-services-core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt b/play-services-core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt deleted file mode 100644 index ab2dc0422e..0000000000 --- a/play-services-core/src/main/kotlin/org/microg/gms/checkin/LastCheckinInfo.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2013-2017 microG Project Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.microg.gms.checkin - -import android.content.Context -import org.microg.gms.settings.SettingsContract -import org.microg.gms.settings.SettingsContract.CheckIn - -data class LastCheckinInfo( - val lastCheckin: Long, - val androidId: Long, - val securityToken: Long, - val digest: String, - val versionInfo: String, - val deviceDataVersionInfo: String, -) { - - constructor(r: CheckinResponse) : this( - lastCheckin = r.timeMs ?: 0L, - androidId = r.androidId ?: 0L, - securityToken = r.securityToken ?: 0L, - digest = r.digest ?: CheckIn.INITIAL_DIGEST, - versionInfo = r.versionInfo ?: "", - deviceDataVersionInfo = r.deviceDataVersionInfo ?: "", - ) - - companion object { - @JvmStatic - fun read(context: Context): LastCheckinInfo { - val projection = arrayOf( - CheckIn.ANDROID_ID, - CheckIn.DIGEST, - CheckIn.LAST_CHECK_IN, - CheckIn.SECURITY_TOKEN, - CheckIn.VERSION_INFO, - CheckIn.DEVICE_DATA_VERSION_INFO, - ) - return SettingsContract.getSettings(context, CheckIn.getContentUri(context), projection) { c -> - LastCheckinInfo( - androidId = c.getLong(0), - digest = c.getString(1), - lastCheckin = c.getLong(2), - securityToken = c.getLong(3), - versionInfo = c.getString(4), - deviceDataVersionInfo = c.getString(5), - ) - } - } - - @JvmStatic - fun clear(context: Context) = SettingsContract.setSettings(context, CheckIn.getContentUri(context)) { - put(CheckIn.ANDROID_ID, 0L) - put(CheckIn.DIGEST, CheckIn.INITIAL_DIGEST) - put(CheckIn.LAST_CHECK_IN, 0L) - put(CheckIn.SECURITY_TOKEN, 0L) - put(CheckIn.VERSION_INFO, "") - put(CheckIn.DEVICE_DATA_VERSION_INFO, "") - } - } - - fun write(context: Context) = SettingsContract.setSettings(context, CheckIn.getContentUri(context)) { - put(CheckIn.ANDROID_ID, androidId) - put(CheckIn.DIGEST, digest) - put(CheckIn.LAST_CHECK_IN, lastCheckin) - put(CheckIn.SECURITY_TOKEN, securityToken) - put(CheckIn.VERSION_INFO, versionInfo) - put(CheckIn.DEVICE_DATA_VERSION_INFO, deviceDataVersionInfo) - } -} diff --git a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt index e4d4cad46a..586656acf4 100644 --- a/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt +++ b/play-services-core/src/main/kotlin/org/microg/gms/games/GamesService.kt @@ -22,7 +22,6 @@ import androidx.lifecycle.lifecycleScope import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.Scopes import com.google.android.gms.common.api.CommonStatusCodes -import com.google.android.gms.common.api.Scope import com.google.android.gms.common.api.Status import com.google.android.gms.common.data.DataHolder import com.google.android.gms.common.internal.ConnectionInfo From c976df1f0cb987b9db7f2ebdbd61b8397a1b47fb Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 19 Oct 2024 10:26:27 +0200 Subject: [PATCH 53/59] Clean up constants --- .../authenticator/WorkAccountAuthenticator.kt | 17 +++++++++-------- .../java/org/microg/gms/auth/AuthConstants.java | 5 +++++ .../vending/WorkAccountChangedReceiver.kt | 3 ++- .../org/microg/vending/ui/VendingActivity.kt | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index f1b8d83127..8eab606064 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -15,6 +15,7 @@ import android.os.Build import android.os.Bundle import android.util.Log import com.google.android.gms.auth.workaccount.R +import org.microg.gms.auth.AuthConstants import org.microg.gms.common.PackageUtils import org.microg.gms.auth.AuthRequest import org.microg.gms.auth.AuthResponse @@ -71,14 +72,17 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica val accountManager = AccountManager.get(context) if (accountManager.addAccountExplicitly( - Account(authResponse.email, WORK_ACCOUNT_TYPE), + Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE), authResponse.token, Bundle().apply { // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. if (authResponse.accountId.isNotBlank()) { putString(KEY_GOOGLE_USER_ID, authResponse.accountId) } - putString(KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) - putString(KEY_ACCOUNT_SERVICES, authResponse.services) // expected to be "android" + putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) + putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) + if (authResponse.services != "android") { + Log.i(TAG, "unexpected 'services' value ${authResponse.services} (usually 'android')") + } } )) { @@ -90,7 +94,7 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica // Report successful creation to caller response.onResult(Bundle().apply { putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email) - putString(AccountManager.KEY_ACCOUNT_TYPE, WORK_ACCOUNT_TYPE) + putString(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.WORK_ACCOUNT_TYPE) }) } @@ -209,13 +213,10 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica companion object { const val TAG = "WorkAccAuthenticator" - const val WORK_ACCOUNT_TYPE = "com.google.work" const val WORK_ACCOUNT_CHANGED_BOARDCAST = "org.microg.vending.WORK_ACCOUNT_CHANGED" const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken" - private const val KEY_GOOGLE_USER_ID = "GoogleUserId" // TODO: use AuthConstants - private const val KEY_ACCOUNT_SERVICES = "services" // TODO: use AuthConstants - private const val KEY_ACCOUNT_CAPABILITIES = "capabilities" + private const val KEY_GOOGLE_USER_ID = AuthConstants.GOOGLE_USER_ID } } \ No newline at end of file diff --git a/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java b/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java index 7de65dafd0..1fa905322b 100644 --- a/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java +++ b/play-services-basement/src/main/java/org/microg/gms/auth/AuthConstants.java @@ -24,6 +24,11 @@ public class AuthConstants { public static final String PROVIDER_EXTRA_CLEAR_PASSWORD = "clear_password"; public static final String PROVIDER_EXTRA_ACCOUNTS = "accounts"; public static final String DEFAULT_ACCOUNT_TYPE = "com.google"; + public static final String WORK_ACCOUNT_TYPE = "com.google.work"; + + public static final String KEY_ACCOUNT_SERVICES = "services"; + public static final String KEY_ACCOUNT_CAPABILITIES = "capabilities"; + public static final String GOOGLE_USER_ID = "GoogleUserId"; public static final String GOOGLE_SIGN_IN_STATUS = "googleSignInStatus"; public static final String GOOGLE_SIGN_IN_ACCOUNT = "googleSignInAccount"; diff --git a/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt b/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt index 32af2f730b..dbdc0a675c 100644 --- a/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt +++ b/vending-app/src/main/java/org/microg/vending/WorkAccountChangedReceiver.kt @@ -8,13 +8,14 @@ import android.content.Intent import android.content.pm.PackageManager import android.os.Build import android.util.Log +import org.microg.gms.auth.AuthConstants import org.microg.vending.ui.VendingActivity class WorkAccountChangedReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent?) { val accountManager = AccountManager.get(context) - val hasWorkAccounts = accountManager.getAccountsByType("com.google.work").isNotEmpty() + val hasWorkAccounts = accountManager.getAccountsByType(AuthConstants.WORK_ACCOUNT_TYPE).isNotEmpty() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 68ae397c0a..f5d6934c36 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -24,6 +24,7 @@ import com.android.vending.buildRequestHeaders import com.android.vending.installer.installPackagesFromNetwork import com.android.vending.installer.uninstallPackage import kotlinx.coroutines.runBlocking +import org.microg.gms.auth.AuthConstants import org.microg.gms.common.DeviceConfiguration import org.microg.gms.common.asProto import org.microg.gms.profile.Build @@ -72,13 +73,12 @@ class VendingActivity : ComponentActivity() { ProfileManager.ensureInitialized(this) val accountManager = AccountManager.get(this) - val accounts = accountManager.getAccountsByType("com.google.work") + val accounts = accountManager.getAccountsByType(AuthConstants.WORK_ACCOUNT_TYPE) if (accounts.isEmpty()) { // Component should not be enabled; disable through receiver, and redirect to main activity WorkAccountChangedReceiver().onReceive(this, null) startActivity(Intent(this, MainActivity::class.java)) finish() - TODO("App should only be visible if work accounts are added. Disable component and wonder why it was enabled in the first place") } else if (accounts.size > 1) { Log.w(TAG, "Multiple work accounts found. This is unexpected and could point " + "towards misuse of the work account service API by the DPC.") From 655cc6fca57609edd73cc8b925368193c9e04101 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 19 Oct 2024 11:56:07 +0200 Subject: [PATCH 54/59] Fix crash when no apps in enterprise policy --- .../main/java/org/microg/vending/ui/VendingActivity.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index f5d6934c36..68f237840c 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -207,6 +207,13 @@ class VendingActivity : ComponentActivity() { Log.v(TAG, "app policy: ${apps.joinToString { "${it.packageName}: ${it.policy}" }}") + if (apps.isEmpty()) { + // Don't fetch details of empty app list (otherwise HTTP 400) + networkState = NetworkState.PASSIVE + this@VendingActivity.apps.clear() + return@runBlocking + } + // Fetch details about all available apps val details = client.post( url = URL_ITEM_DETAILS, @@ -257,7 +264,7 @@ class VendingActivity : ComponentActivity() { networkState = NetworkState.ERROR Log.e(TAG, "Network error: ${e.message}") e.printStackTrace() - } catch (e: NullPointerException) { + } catch (e: Exception) { networkState = NetworkState.ERROR Log.e(TAG, "Unexpected network response, cannot process") e.printStackTrace() From 4f0665c289493b39a4008fe0d29e359ebf87eccd Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 19 Oct 2024 14:39:36 +0200 Subject: [PATCH 55/59] Fix build --- .../org/microg/gms/auth/workaccount/WorkAccountService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt index 488a872631..8db6e0287a 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/org/microg/gms/auth/workaccount/WorkAccountService.kt @@ -20,7 +20,6 @@ import com.google.android.gms.auth.account.IWorkAccountCallback import com.google.android.gms.auth.account.IWorkAccountService import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.KEY_ACCOUNT_CREATION_TOKEN import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_CHANGED_BOARDCAST -import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticator.Companion.WORK_ACCOUNT_TYPE import com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService import com.google.android.gms.common.Feature import com.google.android.gms.common.api.CommonStatusCodes @@ -28,6 +27,7 @@ import com.google.android.gms.common.internal.ConnectionInfo import com.google.android.gms.common.internal.GetServiceRequest import com.google.android.gms.common.internal.IGmsCallbacks import org.microg.gms.BaseService +import org.microg.gms.auth.AuthConstants import org.microg.gms.common.GmsService import org.microg.gms.common.PackageUtils @@ -101,7 +101,7 @@ class WorkAccountServiceImpl(val context: Context) : IWorkAccountService.Stub() ) { Log.d(TAG, "addWorkAccount with token $token") val future = accountManager.addAccount( - WORK_ACCOUNT_TYPE, + AuthConstants.WORK_ACCOUNT_TYPE, null, null, Bundle().apply { putString(KEY_ACCOUNT_CREATION_TOKEN, token) }, From a6dc3b3fd5c775fbc3146f7a5bb7452e90b17798 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 19 Oct 2024 15:02:04 +0200 Subject: [PATCH 56/59] Purchase apps before downloading in work store Purchasing before downloading seems to be necessary in at least some cases for work profiles as well. --- .../org/microg/vending/ui/VendingActivity.kt | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 68f237840c..3137989ba9 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -37,6 +37,7 @@ import org.microg.vending.billing.core.AuthData import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ENTERPRISE_CLIENT_POLICY import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_FDFE import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_ITEM_DETAILS +import org.microg.vending.billing.core.GooglePlayApi.Companion.URL_PURCHASE import org.microg.vending.billing.core.HttpClient import org.microg.vending.billing.createDeviceEnvInfo import org.microg.vending.billing.proto.GoogleApiResponse @@ -97,7 +98,22 @@ class VendingActivity : ComponentActivity() { val client = HttpClient() // Get download links for requested package - val downloadUrls = runCatching { client.requestDownloadUrls( + val downloadUrls = runCatching { + + // Purchase app (only needs to be done once, in theory) + val parameters = mapOf( + "ot" to "1", + "doc" to app.packageName, + "vc" to app.versionCode.toString() + ) + client.post( + url = URL_PURCHASE, + headers = buildRequestHeaders(auth!!.authToken, auth!!.gsfId.toLong(16)), + params = parameters, + adapter = GoogleApiResponse.ADAPTER + ) + + client.requestDownloadUrls( app.packageName, app.versionCode!!.toLong(), auth!!, From b2cec3383ddba578232bdf8f95334345b52c5fbb Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Sat, 19 Oct 2024 15:31:42 +0200 Subject: [PATCH 57/59] Support additional strange behaviors Support unexpected scenarios spotted in the wild on a best-effort basis: * Enterprise policies with apps missing policy fields * App not downloading due to not being purchased --- .../org/microg/vending/ui/VendingActivity.kt | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt index 3137989ba9..98cf846e97 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/VendingActivity.kt @@ -51,6 +51,7 @@ import org.microg.vending.enterprise.NotCompatible import org.microg.vending.enterprise.NotInstalled import org.microg.vending.enterprise.Pending import org.microg.vending.enterprise.UpdateAvailable +import org.microg.vending.enterprise.proto.AppInstallPolicy import org.microg.vending.proto.AppMeta import org.microg.vending.proto.GetItemsRequest import org.microg.vending.proto.RequestApp @@ -97,22 +98,31 @@ class VendingActivity : ComponentActivity() { val client = HttpClient() + // Purchase app (only needs to be done once, in theory – behaviour seems flaky) + // Ignore failures + runCatching { + if (app.policy != AppInstallPolicy.MANDATORY) { + val parameters = mapOf( + "ot" to "1", + "doc" to app.packageName, + "vc" to app.versionCode.toString() + ) + client.post( + url = URL_PURCHASE, + headers = buildRequestHeaders( + auth!!.authToken, + auth!!.gsfId.toLong(16) + ), + params = parameters, + adapter = GoogleApiResponse.ADAPTER + ) + } + }.onFailure { Log.i(TAG, "couldn't purchase ${app.packageName}: ${it.message}") } + .onSuccess { Log.d(TAG, "purchased ${app.packageName} successfully") } + // Get download links for requested package val downloadUrls = runCatching { - // Purchase app (only needs to be done once, in theory) - val parameters = mapOf( - "ot" to "1", - "doc" to app.packageName, - "vc" to app.versionCode.toString() - ) - client.post( - url = URL_PURCHASE, - headers = buildRequestHeaders(auth!!.authToken, auth!!.gsfId.toLong(16)), - params = parameters, - adapter = GoogleApiResponse.ADAPTER - ) - client.requestDownloadUrls( app.packageName, app.versionCode!!.toLong(), @@ -213,7 +223,7 @@ class VendingActivity : ComponentActivity() { url = URL_ENTERPRISE_CLIENT_POLICY, headers = headers.plus("content-type" to "application/x-protobuf"), adapter = GoogleApiResponse.ADAPTER - ).payload?.enterpriseClientPolicyResponse?.policy?.apps?.filter { it.packageName != null && it.policy != null } + ).payload?.enterpriseClientPolicyResponse?.policy?.apps?.filter { it.packageName != null } if (apps == null) { Log.e(TAG, "unexpected network response: missing expected fields") @@ -265,7 +275,7 @@ class VendingActivity : ComponentActivity() { item.detail!!.name!!.displayName!!, item.detail.icon?.icon?.paint?.url, item.offer?.delivery?.key, - apps.find { it.packageName!! == item.meta.packageName }!!.policy!!, + apps.find { it.packageName!! == item.meta.packageName }!!.policy ?: AppInstallPolicy.OPTIONAL, ) to state }.onEach { Log.v(TAG, "${it.key.packageName} (state: ${it.value}) delivery token: ${it.key.deliveryToken ?: "none acquired"}") From 04afa56029a9155c7a925080c7d440874adbcae7 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 21 Oct 2024 13:11:38 +0200 Subject: [PATCH 58/59] Show info: wait before work app store can be used --- .../microg/vending/ui/WorkVendingTopAppBar.kt | 2 +- .../vending/ui/components/EnterpriseList.kt | 24 ++++++++++++++++++- vending-app/src/main/res/values/strings.xml | 3 ++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt b/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt index 5a07b984f6..362546c79d 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/WorkVendingTopAppBar.kt @@ -39,7 +39,7 @@ fun WorkVendingTopAppBar() = TopAppBar( }, colors = TopAppBarDefaults.smallTopAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer ) ) diff --git a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt index 84e217bf80..1f33b42d43 100644 --- a/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt +++ b/vending-app/src/main/java/org/microg/vending/ui/components/EnterpriseList.kt @@ -2,15 +2,18 @@ package org.microg.vending.ui.components import androidx.annotation.StringRes import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -59,11 +62,30 @@ internal fun EnterpriseList(appStates: Map, install: (a .fillMaxSize() .padding(24.dp) ) { - Column(Modifier.align(Alignment.Center)) { + Column(Modifier.align(Alignment.Center), verticalArrangement = Arrangement.spacedBy(32.dp)) { Text( stringResource(R.string.vending_overview_enterprise_no_apps_available), textAlign = TextAlign.Center ) + + Row( + Modifier + .clip(shape = RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.primaryContainer), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Info, + contentDescription = null, + Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp, end = 16.dp), + MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + stringResource(R.string.vending_overview_enterprise_no_apps_available_wait), + Modifier.padding(top = 16.dp, bottom = 16.dp, end = 16.dp), + MaterialTheme.colorScheme.onPrimaryContainer + ) + } } } diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml index a255639d2a..1743b43018 100644 --- a/vending-app/src/main/res/values/strings.xml +++ b/vending-app/src/main/res/values/strings.xml @@ -23,7 +23,8 @@ Your device is missing mandatory apps chosen by your administrator. Available apps These are all the apps made available by your enterprise. - No apps have been made available by your administrator + No apps have been made available by your administrator. + It may take a few hours after setting up your work profile before apps are ready to download. Update available Installed apps Install From ed745dc585d3d4e6972c3f4b84a42cb0d4ade120 Mon Sep 17 00:00:00 2001 From: Fynn Godau Date: Mon, 28 Oct 2024 13:27:40 +0100 Subject: [PATCH 59/59] Apply review --- .../authenticator/WorkAccountAuthenticator.kt | 88 ++++---- vending-app/src/main/AndroidManifest.xml | 2 +- .../vending/WorkAccountChangedReceiver.kt | 4 +- ...VendingActivity.kt => WorkAppsActivity.kt} | 204 +++++++++--------- 4 files changed, 157 insertions(+), 141 deletions(-) rename vending-app/src/main/java/org/microg/vending/ui/{VendingActivity.kt => WorkAppsActivity.kt} (71%) diff --git a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt index 8eab606064..0a0080e952 100644 --- a/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt +++ b/play-services-auth-workaccount/core/src/main/kotlin/com/google/android/gms/auth/account/authenticator/WorkAccountAuthenticator.kt @@ -20,6 +20,7 @@ import org.microg.gms.common.PackageUtils import org.microg.gms.auth.AuthRequest import org.microg.gms.auth.AuthResponse import java.io.IOException +import kotlin.jvm.Throws class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) { @@ -60,44 +61,7 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica val oauthToken: String = options.getString(KEY_ACCOUNT_CREATION_TOKEN)!! try { - val authResponse = AuthRequest().fromContext(context) - .appIsGms() - .callerIsGms() - .service("ac2dm") - .token(oauthToken).isAccessToken() - .addAccount() - .getAccountId() - .droidguardResults(null) - .response - - val accountManager = AccountManager.get(context) - if (accountManager.addAccountExplicitly( - Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE), - authResponse.token, Bundle().apply { - // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. - if (authResponse.accountId.isNotBlank()) { - putString(KEY_GOOGLE_USER_ID, authResponse.accountId) - } - putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) - putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) - if (authResponse.services != "android") { - Log.i(TAG, "unexpected 'services' value ${authResponse.services} (usually 'android')") - } - } - )) { - - // Notify vending package - context.sendBroadcast( - Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending") - ) - - // Report successful creation to caller - response.onResult(Bundle().apply { - putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email) - putString(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.WORK_ACCOUNT_TYPE) - }) - } - + tryAddAccount(oauthToken, response) } catch (exception: Exception) { response.onResult(Bundle().apply { putInt( @@ -116,6 +80,54 @@ class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthentica return null } + @Throws(Exception::class) + private fun tryAddAccount( + oauthToken: String, + response: AccountAuthenticatorResponse + ) { + val authResponse = AuthRequest().fromContext(context) + .appIsGms() + .callerIsGms() + .service("ac2dm") + .token(oauthToken).isAccessToken() + .addAccount() + .getAccountId() + .droidguardResults(null) + .response + + val accountManager = AccountManager.get(context) + if (accountManager.addAccountExplicitly( + Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE), + authResponse.token, Bundle().apply { + // Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name. + if (authResponse.accountId.isNotBlank()) { + putString(KEY_GOOGLE_USER_ID, authResponse.accountId) + } + putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities) + putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services) + if (authResponse.services != "android") { + Log.i( + TAG, + "unexpected 'services' value ${authResponse.services} (usually 'android')" + ) + } + } + ) + ) { + + // Notify vending package + context.sendBroadcast( + Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending") + ) + + // Report successful creation to caller + response.onResult(Bundle().apply { + putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email) + putString(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.WORK_ACCOUNT_TYPE) + }) + } + } + override fun confirmCredentials( response: AccountAuthenticatorResponse?, account: Account?, diff --git a/vending-app/src/main/AndroidManifest.xml b/vending-app/src/main/AndroidManifest.xml index 16a64c792c..c15f626fa7 100644 --- a/vending-app/src/main/AndroidManifest.xml +++ b/vending-app/src/main/AndroidManifest.xml @@ -194,7 +194,7 @@ android:exported="false"/> - = mutableStateMapOf() private var networkState by mutableStateOf(NetworkState.ACTIVE) @@ -89,99 +89,6 @@ class VendingActivity : ComponentActivity() { load(account) - val install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit = { app, isUpdate -> - Thread { - runBlocking { - - val previousState = apps[app]!! - apps[app] = Pending - - val client = HttpClient() - - // Purchase app (only needs to be done once, in theory – behaviour seems flaky) - // Ignore failures - runCatching { - if (app.policy != AppInstallPolicy.MANDATORY) { - val parameters = mapOf( - "ot" to "1", - "doc" to app.packageName, - "vc" to app.versionCode.toString() - ) - client.post( - url = URL_PURCHASE, - headers = buildRequestHeaders( - auth!!.authToken, - auth!!.gsfId.toLong(16) - ), - params = parameters, - adapter = GoogleApiResponse.ADAPTER - ) - } - }.onFailure { Log.i(TAG, "couldn't purchase ${app.packageName}: ${it.message}") } - .onSuccess { Log.d(TAG, "purchased ${app.packageName} successfully") } - - // Get download links for requested package - val downloadUrls = runCatching { - - client.requestDownloadUrls( - app.packageName, - app.versionCode!!.toLong(), - auth!!, - deliveryToken = app.deliveryToken - ) } - - if (downloadUrls.isFailure) { - Log.w(TAG, "Failed to request download URLs: ${downloadUrls.exceptionOrNull()!!.message}") - apps[app] = previousState - return@runBlocking - } - - runCatching { - - var lastNotification = 0L - installPackagesFromNetwork( - packageName = app.packageName, - components = downloadUrls.getOrThrow(), - httpClient = client, - isUpdate = isUpdate - ) { session, progress -> - - - // Android rate limits notification updates by some vague rule of "not too many in less than one second" - if (progress !is Downloading || lastNotification + 250 < System.currentTimeMillis()) { - notifyInstallProgress(app.displayName, session, progress) - lastNotification = System.currentTimeMillis() - } - - if (progress is Downloading) apps[app] = progress - else if (progress is CommitingSession) apps[app] = Pending - } - }.onSuccess { - apps[app] = Installed - }.onFailure { exception -> - Log.w(TAG, "Installation from network unsuccessful.", exception) - apps[app] = previousState - } - } - }.start() - } - - val uninstall: (app: EnterpriseApp) -> Unit = { app -> - Thread { - runBlocking { - - val previousState = apps[app]!! - apps[app] = Pending - runCatching { uninstallPackage(app.packageName) }.onSuccess { - apps[app] = NotInstalled - }.onFailure { - apps[app] = previousState - } - - } - }.start() - } - setContent { VendingUi(account, install, uninstall) } @@ -193,9 +100,9 @@ class VendingActivity : ComponentActivity() { runBlocking { try { // Authenticate - auth = AuthManager.getAuthData(this@VendingActivity, account) + auth = AuthManager.getAuthData(this@WorkAppsActivity, account) val authData = auth - val deviceInfo = createDeviceEnvInfo(this@VendingActivity) + val deviceInfo = createDeviceEnvInfo(this@WorkAppsActivity) if (deviceInfo == null || authData == null) { Log.e(TAG, "Unable to open play store when deviceInfo = $deviceInfo and authData = $authData") networkState = NetworkState.ERROR @@ -210,7 +117,7 @@ class VendingActivity : ComponentActivity() { url = "$URL_FDFE/uploadDeviceConfig", headers = headers.minus("X-PS-RH"), payload = UploadDeviceConfigRequest( - DeviceConfiguration(this@VendingActivity).asProto(), + DeviceConfiguration(this@WorkAppsActivity).asProto(), manufacturer = Build.MANUFACTURER, //gcmRegistrationId = TODO: looks like remote-triggered app downloads may be announced through GCM? ), @@ -236,7 +143,7 @@ class VendingActivity : ComponentActivity() { if (apps.isEmpty()) { // Don't fetch details of empty app list (otherwise HTTP 400) networkState = NetworkState.PASSIVE - this@VendingActivity.apps.clear() + this@WorkAppsActivity.apps.clear() return@runBlocking } @@ -253,7 +160,7 @@ class VendingActivity : ComponentActivity() { adapter = GoogleApiResponse.ADAPTER ).getItemsResponses.mapNotNull { it.response }.associate { item -> val packageName = item.meta!!.packageName!! - val installedDetails = this@VendingActivity.packageManager.getInstalledPackages(0).find { + val installedDetails = this@WorkAppsActivity.packageManager.getInstalledPackages(0).find { it.applicationInfo.packageName == packageName } @@ -281,7 +188,7 @@ class VendingActivity : ComponentActivity() { Log.v(TAG, "${it.key.packageName} (state: ${it.value}) delivery token: ${it.key.deliveryToken ?: "none acquired"}") } - this@VendingActivity.apps.apply { + this@WorkAppsActivity.apps.apply { clear() putAll(details) } @@ -300,6 +207,99 @@ class VendingActivity : ComponentActivity() { } + private val install: (app: EnterpriseApp, isUpdate: Boolean) -> Unit = { app, isUpdate -> + Thread { + runBlocking { + + val previousState = apps[app]!! + apps[app] = Pending + + val client = HttpClient() + + // Purchase app (only needs to be done once, in theory – behaviour seems flaky) + // Ignore failures + runCatching { + if (app.policy != AppInstallPolicy.MANDATORY) { + val parameters = mapOf( + "ot" to "1", + "doc" to app.packageName, + "vc" to app.versionCode.toString() + ) + client.post( + url = URL_PURCHASE, + headers = buildRequestHeaders( + auth!!.authToken, + auth!!.gsfId.toLong(16) + ), + params = parameters, + adapter = GoogleApiResponse.ADAPTER + ) + } + }.onFailure { Log.i(TAG, "couldn't purchase ${app.packageName}: ${it.message}") } + .onSuccess { Log.d(TAG, "purchased ${app.packageName} successfully") } + + // Get download links for requested package + val downloadUrls = runCatching { + + client.requestDownloadUrls( + app.packageName, + app.versionCode!!.toLong(), + auth!!, + deliveryToken = app.deliveryToken + ) } + + if (downloadUrls.isFailure) { + Log.w(TAG, "Failed to request download URLs: ${downloadUrls.exceptionOrNull()!!.message}") + apps[app] = previousState + return@runBlocking + } + + runCatching { + + var lastNotification = 0L + installPackagesFromNetwork( + packageName = app.packageName, + components = downloadUrls.getOrThrow(), + httpClient = client, + isUpdate = isUpdate + ) { session, progress -> + + + // Android rate limits notification updates by some vague rule of "not too many in less than one second" + if (progress !is Downloading || lastNotification + 250 < System.currentTimeMillis()) { + notifyInstallProgress(app.displayName, session, progress) + lastNotification = System.currentTimeMillis() + } + + if (progress is Downloading) apps[app] = progress + else if (progress is CommitingSession) apps[app] = Pending + } + }.onSuccess { + apps[app] = Installed + }.onFailure { exception -> + Log.w(TAG, "Installation from network unsuccessful.", exception) + apps[app] = previousState + } + } + }.start() + } + + private val uninstall: (app: EnterpriseApp) -> Unit = { app -> + Thread { + runBlocking { + + val previousState = apps[app]!! + apps[app] = Pending + runCatching { uninstallPackage(app.packageName) }.onSuccess { + apps[app] = NotInstalled + }.onFailure { + apps[app] = previousState + } + + } + }.start() + } + @OptIn(ExperimentalMaterial3Api::class) @Composable fun VendingUi( @@ -321,4 +321,8 @@ class VendingActivity : ComponentActivity() { } } } + + companion object { + const val TAG = "GmsVendingWorkApp" + } } \ No newline at end of file