Skip to content

Commit 53c0aac

Browse files
Fix the issue requiring a security verification flow for in-app subscriptions (#3507)
Co-authored-by: Marvin W <git@larma.de>
1 parent c1f0efb commit 53c0aac

75 files changed

Lines changed: 5991 additions & 1341 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ buildscript {
3131
ext.volleyVersion = '1.2.1'
3232
ext.okHttpVersion = '4.12.0'
3333
ext.ktorVersion = '2.3.12'
34-
ext.wireVersion = '4.9.9'
34+
ext.wireVersion = '6.2.0'
3535
ext.tinkVersion = '1.13.0'
36+
ext.guavaVersion = '33.5.0-android'
3637

37-
ext.androidBuildGradleVersion = '8.2.2'
38+
ext.androidBuildGradleVersion = '8.13.2'
3839

39-
ext.androidBuildVersionTools = '34.0.0'
40+
ext.androidBuildVersionTools = '35.0.0'
4041

4142
ext.androidMinSdk = 19
4243
ext.androidTargetSdk = 29
@@ -64,6 +65,7 @@ buildscript {
6465
dependencies {
6566
classpath "com.android.tools.build:gradle:$androidBuildGradleVersion"
6667
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
68+
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion"
6769
classpath "com.squareup.wire:wire-gradle-plugin:$wireVersion"
6870
}
6971
}

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
distributionBase=GRADLE_USER_HOME
55
distributionPath=wrapper/dists
6-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
6+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
77
networkTimeout=10000
88
validateDistributionUrl=true
99
zipStoreBase=GRADLE_USER_HOME

play-services-api/build.gradle

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,28 @@ dependencies {
4545

4646
annotationProcessor project(':safe-parcel-processor')
4747
}
48+
49+
// build-tools 35.0.0 aidl.exe on Windows writes a `* Using: <full path>` header into generated
50+
// *.java files. For AIDL packages containing `\u<non-hex>` (here `\usagereporting`), javac treats
51+
// it as an illegal Java Unicode escape per JLS 3.3 (escapes are processed even inside comments)
52+
// and fails. Strip that line right after AIDL compilation.
53+
// TODO: remove once build-tools is upgraded past the version with this bug.
54+
afterEvaluate {
55+
tasks.matching { it.name ==~ /compile.*Aidl/ }.configureEach {
56+
doLast { task ->
57+
task.outputs.files.each { outRoot ->
58+
if (outRoot.exists()) {
59+
outRoot.eachFileRecurse { javaFile ->
60+
if (javaFile.isFile() && javaFile.name.endsWith('.java')) {
61+
def original = javaFile.text
62+
def stripped = original.replaceAll(/(?m)^[ \t]*\*[ \t]+Using:.*\R?/, '')
63+
if (!original.equals(stripped)) {
64+
javaFile.text = stripped
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
}
72+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.google.android.gms.icing.service;
7+
8+
interface IAppIndexingService {
9+
}

vending-app/src/main/java/org/microg/vending/billing/core/DeviceEnvInfo.kt renamed to play-services-base/core/src/main/kotlin/org/microg/gms/deviceinfo/DeviceEnvInfo.kt

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
package org.microg.vending.billing.core
1+
/*
2+
* SPDX-FileCopyrightText: 2025 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.deviceinfo
27

38
import java.util.Locale
49

@@ -33,7 +38,15 @@ data class DeviceEnvInfo(
3338
val installNonMarketApps: Boolean,
3439
val uptimeMillis: Long,
3540
val timeZoneDisplayName: String,
36-
val googleAccounts: List<String>
41+
val googleAccounts: List<String>,
42+
43+
val sdkVersion: String? = null,
44+
val gmsPackageName: String? = null,
45+
val cameraPermissionState: Int = -1,
46+
val isInCallOrRingMode: Boolean = false,
47+
val isUsbConnected: Boolean = false,
48+
val isCharging: Boolean = false,
49+
val screenBrightness: Int = -1
3750
)
3851

3952
data class DisplayMetrics(
@@ -49,7 +62,10 @@ data class TelephonyData(
4962
val phoneDeviceId: String,
5063
val networkOperator: String,
5164
val simOperator: String,
52-
val phoneType: Int = -1
65+
val phoneType: Int = -1,
66+
val grantedPhonePermissionState: Int = -1,
67+
val isSmsCapable: Boolean = false,
68+
val activeSubscriptionInfoCount: Int = 0
5369
)
5470

5571
data class LocationData(
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.deviceinfo
7+
8+
import android.Manifest
9+
import android.accounts.AccountManager
10+
import android.annotation.SuppressLint
11+
import android.content.Context
12+
import android.content.Intent
13+
import android.content.IntentFilter
14+
import android.content.pm.PackageManager
15+
import android.hardware.usb.UsbManager
16+
import android.icu.util.TimeZone
17+
import android.media.AudioManager
18+
import android.net.ConnectivityManager
19+
import android.os.Build.VERSION.SDK_INT
20+
import android.os.SystemClock
21+
import android.provider.Settings
22+
import android.util.Base64
23+
import android.util.Log
24+
import android.view.WindowManager
25+
import androidx.core.content.ContextCompat
26+
import org.microg.gms.auth.AuthConstants
27+
import org.microg.gms.common.Constants
28+
import org.microg.gms.common.DeviceIdentifier
29+
import org.microg.gms.profile.Build
30+
import org.microg.gms.utils.digest
31+
import org.microg.gms.utils.toBase64
32+
import java.util.Locale
33+
34+
private const val TAG = "DeviceInfoCollector"
35+
36+
@SuppressLint("MissingPermission")
37+
fun getDeviceIdentifier(context: Context): String {
38+
// TODO: Improve dummy data
39+
val deviceId = DeviceIdentifier().meid /*try {
40+
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.let {
41+
it.subscriberId ?: it.deviceId
42+
}
43+
} catch (e: Exception) {
44+
null
45+
}*/
46+
return deviceId.toByteArray(Charsets.UTF_8).digest("SHA-1")
47+
.toBase64(Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING)
48+
}
49+
50+
fun getDisplayInfo(context: Context): DisplayMetrics? {
51+
return try {
52+
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
53+
if (windowManager != null) {
54+
val displayMetrics = android.util.DisplayMetrics()
55+
windowManager.defaultDisplay.getRealMetrics(displayMetrics)
56+
return DisplayMetrics(
57+
displayMetrics.widthPixels,
58+
displayMetrics.heightPixels,
59+
displayMetrics.xdpi,
60+
displayMetrics.ydpi,
61+
displayMetrics.densityDpi
62+
)
63+
}
64+
return DisplayMetrics(
65+
context.resources.displayMetrics.widthPixels,
66+
context.resources.displayMetrics.heightPixels,
67+
context.resources.displayMetrics.xdpi,
68+
context.resources.displayMetrics.ydpi,
69+
context.resources.displayMetrics.densityDpi
70+
)
71+
} catch (e: Exception) {
72+
null
73+
}
74+
}
75+
76+
// TODO: Improve privacy
77+
fun getBatteryLevel(context: Context): Int {
78+
var batteryLevel = -1
79+
val intentFilter = IntentFilter("android.intent.action.BATTERY_CHANGED")
80+
context.registerReceiver(null, intentFilter)?.let {
81+
val level = it.getIntExtra("level", -1)
82+
val scale = it.getIntExtra("scale", -1)
83+
if (scale > 0) {
84+
batteryLevel = level * 100 / scale
85+
}
86+
}
87+
if (batteryLevel == -1 && SDK_INT >= 33) {
88+
context.registerReceiver(null, intentFilter, Context.RECEIVER_EXPORTED)?.let {
89+
val level = it.getIntExtra("level", -1)
90+
val scale = it.getIntExtra("scale", -1)
91+
if (scale > 0) {
92+
batteryLevel = level * 100 / scale
93+
}
94+
}
95+
}
96+
return batteryLevel
97+
}
98+
99+
fun getTelephonyData(context: Context): TelephonyData? {
100+
// TODO: Dummy data
101+
return null /*try {
102+
context.getSystemService(Context.TELEPHONY_SERVICE)?.let {
103+
val telephonyManager = it as TelephonyManager
104+
return TelephonyData(
105+
telephonyManager.simOperatorName!!,
106+
DeviceIdentifier.meid,
107+
telephonyManager.networkOperator!!,
108+
telephonyManager.simOperator!!,
109+
telephonyManager.phoneType
110+
)
111+
}
112+
} catch (e: Exception) {
113+
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getTelephonyData", e)
114+
null
115+
}*/
116+
}
117+
118+
@SuppressLint("MissingPermission")
119+
fun getLocationData(context: Context): LocationData? {
120+
// TODO: Dummy data
121+
return null /*try {
122+
(context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?)?.let { locationManager ->
123+
locationManager.getLastKnownLocation("network")?.let { location ->
124+
return LocationData(
125+
location.altitude,
126+
location.latitude,
127+
location.longitude,
128+
location.accuracy,
129+
location.time.toDouble()
130+
)
131+
}
132+
}
133+
} catch (e: Exception) {
134+
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getLocationData", e)
135+
null
136+
}*/
137+
}
138+
139+
@SuppressLint("MissingPermission")
140+
fun getNetworkData(context: Context): NetworkData {
141+
val connectivityManager =
142+
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
143+
val linkDownstreamBandwidth: Long = 0
144+
val linkUpstreamBandwidth: Long = 0
145+
// TODO: Dummy data — populate bandwidth via NetworkCapabilities when permission available
146+
val isActiveNetworkMetered = connectivityManager?.isActiveNetworkMetered ?: false
147+
val netAddressList = mutableListOf<String>()
148+
// TODO: Dummy data — enumerate NetworkInterface inet addresses
149+
return NetworkData(
150+
linkDownstreamBandwidth,
151+
linkUpstreamBandwidth,
152+
isActiveNetworkMetered,
153+
netAddressList
154+
)
155+
}
156+
157+
@SuppressLint("HardwareIds")
158+
fun getAndroidId(context: Context): String =
159+
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: ""
160+
161+
fun isCharging(context: Context): Boolean {
162+
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
163+
val intent = if (Build.VERSION.SDK_INT < 33) {
164+
context.registerReceiver(null, intentFilter)
165+
} else {
166+
context.registerReceiver(null, intentFilter, null, null)
167+
}
168+
return intent?.let {
169+
val status = it.getIntExtra("status", -1)
170+
status == 2 || status == 5
171+
} ?: false
172+
}
173+
174+
fun isInCallOrRingMode(context: Context): Boolean {
175+
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
176+
return audioManager?.let {
177+
when (it.mode) {
178+
AudioManager.MODE_IN_CALL, AudioManager.MODE_RINGTONE -> true
179+
else -> false
180+
}
181+
} ?: false
182+
}
183+
184+
fun isUsbConnected(context: Context): Boolean {
185+
val usbManager = context.getSystemService(Context.USB_SERVICE) as? UsbManager
186+
val packageManager = context.packageManager
187+
return if (usbManager != null &&
188+
(packageManager.hasSystemFeature("android.hardware.usb.host") ||
189+
packageManager.hasSystemFeature("android.hardware.usb.accessory"))
190+
) {
191+
try {
192+
val accessoryList = usbManager.accessoryList
193+
val deviceList = usbManager.deviceList
194+
!(accessoryList == null && deviceList.isEmpty())
195+
} catch (e: NullPointerException) {
196+
false
197+
}
198+
} else {
199+
false
200+
}
201+
}
202+
203+
fun getScreenBrightness(context: Context): Int {
204+
return try {
205+
Settings.System.getInt(context.contentResolver, "screen_brightness")
206+
} catch (e: Settings.SettingNotFoundException) {
207+
-1
208+
}
209+
}
210+
211+
private fun Context.hasPermission(permission: String): Boolean =
212+
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
213+
214+
/**
215+
* Build a [DeviceEnvInfo] for a payments / vending session.
216+
*
217+
* Module-specific parameters (gpVersionCode / gpVersionName / gpPkgName / userAgent)
218+
* must be supplied because each module identifies itself with its own version string.
219+
*
220+
* All payments-protocol extension fields (sdkVersion / gmsPackageName / camera permission /
221+
* battery / USB / call mode / screen brightness) are always collected and populated —
222+
* vending downstream ignores fields it doesn't send.
223+
*
224+
* Static device info (DEVICE / PRODUCT / SERIAL / …) is read through the profile-aware
225+
* [Build] wrapper, so test profiles override what the system would expose.
226+
*
227+
* @return null if the package info lookup fails or any inner collector throws
228+
*/
229+
@SuppressLint("MissingPermission")
230+
fun createDeviceEnvInfo(
231+
context: Context,
232+
gpVersionCode: Long,
233+
gpVersionName: String,
234+
gpPkgName: String,
235+
userAgent: String = "",
236+
): DeviceEnvInfo? {
237+
return try {
238+
val packageInfo = context.packageManager.getPackageInfo(Constants.VENDING_PACKAGE_NAME, 0)
239+
Log.d(TAG, "createDeviceEnvInfo: pkg=${packageInfo.packageName} ver=${packageInfo.versionName}/${packageInfo.versionCode}")
240+
DeviceEnvInfo(
241+
gpVersionCode = gpVersionCode,
242+
gpVersionName = gpVersionName,
243+
gpPkgName = gpPkgName,
244+
gpLastUpdateTime = packageInfo.lastUpdateTime,
245+
gpFirstInstallTime = packageInfo.firstInstallTime,
246+
gpSourceDir = packageInfo.applicationInfo!!.sourceDir!!,
247+
androidId = getAndroidId(context),
248+
biometricSupport = true,
249+
biometricSupportCDD = true,
250+
deviceId = getDeviceIdentifier(context),
251+
serialNo = Build.SERIAL ?: "",
252+
locale = Locale.getDefault(),
253+
userAgent = userAgent,
254+
device = Build.DEVICE ?: "",
255+
displayMetrics = getDisplayInfo(context),
256+
telephonyData = getTelephonyData(context),
257+
locationData = getLocationData(context),
258+
networkData = getNetworkData(context),
259+
product = Build.PRODUCT ?: "",
260+
model = Build.MODEL ?: "",
261+
manufacturer = Build.MANUFACTURER ?: "",
262+
fingerprint = Build.FINGERPRINT ?: "",
263+
release = Build.VERSION.RELEASE ?: "",
264+
brand = Build.BRAND ?: "",
265+
batteryLevel = getBatteryLevel(context),
266+
timeZoneOffset = if (SDK_INT >= 24) TimeZone.getDefault().rawOffset.toLong() else 0,
267+
isAdbEnabled = false,
268+
installNonMarketApps = true,
269+
uptimeMillis = SystemClock.uptimeMillis(),
270+
timeZoneDisplayName = if (SDK_INT >= 24) TimeZone.getDefault().displayName!! else "",
271+
googleAccounts = AccountManager.get(context)
272+
.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE).map { it.name },
273+
sdkVersion = SDK_INT.toString(),
274+
gmsPackageName = Constants.GMS_PACKAGE_NAME,
275+
cameraPermissionState = if (context.hasPermission(Manifest.permission.CAMERA)) 1 else 2,
276+
isInCallOrRingMode = isInCallOrRingMode(context),
277+
isUsbConnected = isUsbConnected(context),
278+
isCharging = isCharging(context),
279+
screenBrightness = getScreenBrightness(context),
280+
)
281+
} catch (e: Exception) {
282+
Log.w(TAG, "createDeviceEnvInfo", e)
283+
null
284+
}
285+
}

0 commit comments

Comments
 (0)