Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ buildscript {
ext.volleyVersion = '1.2.1'
ext.okHttpVersion = '4.12.0'
ext.ktorVersion = '2.3.12'
ext.wireVersion = '4.9.9'
ext.wireVersion = '6.2.0'
ext.tinkVersion = '1.13.0'
ext.guavaVersion = '33.5.0-android'

ext.androidBuildGradleVersion = '8.2.2'
ext.androidBuildGradleVersion = '8.13.0'

ext.androidBuildVersionTools = '34.0.0'

Expand Down Expand Up @@ -64,6 +65,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:$androidBuildGradleVersion"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlinVersion"
classpath "com.squareup.wire:wire-gradle-plugin:$wireVersion"
}
}
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
25 changes: 25 additions & 0 deletions play-services-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,28 @@ dependencies {

annotationProcessor project(':safe-parcel-processor')
}

// build-tools 35.0.0 aidl.exe on Windows writes a `* Using: <full path>` header into generated
// *.java files. For AIDL packages containing `\u<non-hex>` (here `\usagereporting`), javac treats
// it as an illegal Java Unicode escape per JLS 3.3 (escapes are processed even inside comments)
// and fails. Strip that line right after AIDL compilation.
// TODO: remove once build-tools is upgraded past the version with this bug.
afterEvaluate {
tasks.matching { it.name ==~ /compile.*Aidl/ }.configureEach {
doLast { task ->
task.outputs.files.each { outRoot ->
if (outRoot.exists()) {
outRoot.eachFileRecurse { javaFile ->
if (javaFile.isFile() && javaFile.name.endsWith('.java')) {
def original = javaFile.text
def stripped = original.replaceAll(/(?m)^[ \t]*\*[ \t]+Using:.*\R?/, '')
if (!original.equals(stripped)) {
javaFile.text = stripped
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2026 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package com.google.android.gms.icing.service;

interface IAppIndexingService {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
package org.microg.vending.billing.core
/*
* SPDX-FileCopyrightText: 2025 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.deviceinfo

import java.util.Locale

Expand Down Expand Up @@ -33,7 +38,15 @@ data class DeviceEnvInfo(
val installNonMarketApps: Boolean,
val uptimeMillis: Long,
val timeZoneDisplayName: String,
val googleAccounts: List<String>
val googleAccounts: List<String>,

val sdkVersion: String? = null,
val gmsPackageName: String? = null,
val cameraPermissionState: Int = -1,
val isInCallOrRingMode: Boolean = false,
val isUsbConnected: Boolean = false,
val isCharging: Boolean = false,
val screenBrightness: Int = -1
)

data class DisplayMetrics(
Expand All @@ -49,7 +62,10 @@ data class TelephonyData(
val phoneDeviceId: String,
val networkOperator: String,
val simOperator: String,
val phoneType: Int = -1
val phoneType: Int = -1,
val grantedPhonePermissionState: Int = -1,
val isSmsCapable: Boolean = false,
val activeSubscriptionInfoCount: Int = 0
)

data class LocationData(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
/*
* SPDX-FileCopyrightText: 2026 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/

package org.microg.gms.deviceinfo

import android.Manifest
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.hardware.usb.UsbManager
import android.icu.util.TimeZone
import android.media.AudioManager
import android.net.ConnectivityManager
import android.os.Build.VERSION.SDK_INT
import android.os.SystemClock
import android.provider.Settings
import android.util.Base64
import android.util.Log
import android.view.WindowManager
import androidx.core.content.ContextCompat
import org.microg.gms.auth.AuthConstants
import org.microg.gms.common.Constants
import org.microg.gms.common.DeviceIdentifier
import org.microg.gms.profile.Build
import org.microg.gms.utils.digest
import org.microg.gms.utils.toBase64
import java.util.Locale

private const val TAG = "DeviceInfoCollector"

@SuppressLint("MissingPermission")
fun getDeviceIdentifier(context: Context): String {
// TODO: Improve dummy data
val deviceId = DeviceIdentifier().meid /*try {
(context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?)?.let {
it.subscriberId ?: it.deviceId
}
} catch (e: Exception) {
null
}*/
return deviceId.toByteArray(Charsets.UTF_8).digest("SHA-1")
.toBase64(Base64.URL_SAFE + Base64.NO_WRAP + Base64.NO_PADDING)
}

fun getDisplayInfo(context: Context): DisplayMetrics? {
return try {
val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager?
if (windowManager != null) {
val displayMetrics = android.util.DisplayMetrics()
windowManager.defaultDisplay.getRealMetrics(displayMetrics)
return DisplayMetrics(
displayMetrics.widthPixels,
displayMetrics.heightPixels,
displayMetrics.xdpi,
displayMetrics.ydpi,
displayMetrics.densityDpi
)
}
return DisplayMetrics(
context.resources.displayMetrics.widthPixels,
context.resources.displayMetrics.heightPixels,
context.resources.displayMetrics.xdpi,
context.resources.displayMetrics.ydpi,
context.resources.displayMetrics.densityDpi
)
} catch (e: Exception) {
null
}
}

// TODO: Improve privacy
fun getBatteryLevel(context: Context): Int {
var batteryLevel = -1
val intentFilter = IntentFilter("android.intent.action.BATTERY_CHANGED")
context.registerReceiver(null, intentFilter)?.let {
val level = it.getIntExtra("level", -1)
val scale = it.getIntExtra("scale", -1)
if (scale > 0) {
batteryLevel = level * 100 / scale
}
}
if (batteryLevel == -1 && SDK_INT >= 33) {
context.registerReceiver(null, intentFilter, Context.RECEIVER_EXPORTED)?.let {
val level = it.getIntExtra("level", -1)
val scale = it.getIntExtra("scale", -1)
if (scale > 0) {
batteryLevel = level * 100 / scale
}
}
}
return batteryLevel
}

fun getTelephonyData(context: Context): TelephonyData? {
// TODO: Dummy data
return null /*try {
context.getSystemService(Context.TELEPHONY_SERVICE)?.let {
val telephonyManager = it as TelephonyManager
return TelephonyData(
telephonyManager.simOperatorName!!,
DeviceIdentifier.meid,
telephonyManager.networkOperator!!,
telephonyManager.simOperator!!,
telephonyManager.phoneType
)
}
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getTelephonyData", e)
null
}*/
}

@SuppressLint("MissingPermission")
fun getLocationData(context: Context): LocationData? {
// TODO: Dummy data
return null /*try {
(context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?)?.let { locationManager ->
locationManager.getLastKnownLocation("network")?.let { location ->
return LocationData(
location.altitude,
location.latitude,
location.longitude,
location.accuracy,
location.time.toDouble()
)
}
}
} catch (e: Exception) {
if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, "getLocationData", e)
null
}*/
}

@SuppressLint("MissingPermission")
fun getNetworkData(context: Context): NetworkData {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
val linkDownstreamBandwidth: Long = 0
val linkUpstreamBandwidth: Long = 0
// TODO: Dummy data — populate bandwidth via NetworkCapabilities when permission available
val isActiveNetworkMetered = connectivityManager?.isActiveNetworkMetered ?: false
val netAddressList = mutableListOf<String>()
// TODO: Dummy data — enumerate NetworkInterface inet addresses
return NetworkData(
linkDownstreamBandwidth,
linkUpstreamBandwidth,
isActiveNetworkMetered,
netAddressList
)
}

@SuppressLint("HardwareIds")
fun getAndroidId(context: Context): String =
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: ""

fun isCharging(context: Context): Boolean {
val intentFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val intent = if (Build.VERSION.SDK_INT < 33) {
context.registerReceiver(null, intentFilter)
} else {
context.registerReceiver(null, intentFilter, null, null)
}
return intent?.let {
val status = it.getIntExtra("status", -1)
status == 2 || status == 5
} ?: false
}

fun isInCallOrRingMode(context: Context): Boolean {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
return audioManager?.let {
when (it.mode) {
AudioManager.MODE_IN_CALL, AudioManager.MODE_RINGTONE -> true
else -> false
}
} ?: false
}

fun isUsbConnected(context: Context): Boolean {
val usbManager = context.getSystemService(Context.USB_SERVICE) as? UsbManager
val packageManager = context.packageManager
return if (usbManager != null &&
(packageManager.hasSystemFeature("android.hardware.usb.host") ||
packageManager.hasSystemFeature("android.hardware.usb.accessory"))
) {
try {
val accessoryList = usbManager.accessoryList
val deviceList = usbManager.deviceList
!(accessoryList == null && deviceList.isEmpty())
} catch (e: NullPointerException) {
false
}
} else {
false
}
}

fun getScreenBrightness(context: Context): Int {
return try {
Settings.System.getInt(context.contentResolver, "screen_brightness")
} catch (e: Settings.SettingNotFoundException) {
-1
}
}

private fun Context.hasPermission(permission: String): Boolean =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED

/**
* Build a [DeviceEnvInfo] for a payments / vending session.
*
* Module-specific parameters (gpVersionCode / gpVersionName / gpPkgName / userAgent)
* must be supplied because each module identifies itself with its own version string.
*
* All payments-protocol extension fields (sdkVersion / gmsPackageName / camera permission /
* battery / USB / call mode / screen brightness) are always collected and populated —
* vending downstream ignores fields it doesn't send.
*
* Static device info (DEVICE / PRODUCT / SERIAL / …) is read through the profile-aware
* [Build] wrapper, so test profiles override what the system would expose.
*
* @return null if the package info lookup fails or any inner collector throws
*/
@SuppressLint("MissingPermission")
fun createDeviceEnvInfo(
context: Context,
gpVersionCode: Long,
gpVersionName: String,
gpPkgName: String,
userAgent: String = "",
): DeviceEnvInfo? {
return try {
val packageInfo = context.packageManager.getPackageInfo(Constants.VENDING_PACKAGE_NAME, 0)
Log.d(TAG, "createDeviceEnvInfo: pkg=${packageInfo.packageName} ver=${packageInfo.versionName}/${packageInfo.versionCode}")
DeviceEnvInfo(
gpVersionCode = gpVersionCode,
gpVersionName = gpVersionName,
gpPkgName = gpPkgName,
gpLastUpdateTime = packageInfo.lastUpdateTime,
gpFirstInstallTime = packageInfo.firstInstallTime,
gpSourceDir = packageInfo.applicationInfo!!.sourceDir!!,
androidId = getAndroidId(context),
biometricSupport = true,
biometricSupportCDD = true,
deviceId = getDeviceIdentifier(context),
serialNo = Build.SERIAL ?: "",
locale = Locale.getDefault(),
userAgent = userAgent,
device = Build.DEVICE ?: "",
displayMetrics = getDisplayInfo(context),
telephonyData = getTelephonyData(context),
locationData = getLocationData(context),
networkData = getNetworkData(context),
product = Build.PRODUCT ?: "",
model = Build.MODEL ?: "",
manufacturer = Build.MANUFACTURER ?: "",
fingerprint = Build.FINGERPRINT ?: "",
release = Build.VERSION.RELEASE ?: "",
brand = Build.BRAND ?: "",
batteryLevel = getBatteryLevel(context),
timeZoneOffset = if (SDK_INT >= 24) TimeZone.getDefault().rawOffset.toLong() else 0,
isAdbEnabled = false,
installNonMarketApps = true,
uptimeMillis = SystemClock.uptimeMillis(),
timeZoneDisplayName = if (SDK_INT >= 24) TimeZone.getDefault().displayName!! else "",
googleAccounts = AccountManager.get(context)
.getAccountsByType(AuthConstants.DEFAULT_ACCOUNT_TYPE).map { it.name },
sdkVersion = SDK_INT.toString(),
gmsPackageName = Constants.GMS_PACKAGE_NAME,
cameraPermissionState = if (context.hasPermission(Manifest.permission.CAMERA)) 1 else 2,
isInCallOrRingMode = isInCallOrRingMode(context),
isUsbConnected = isUsbConnected(context),
isCharging = isCharging(context),
screenBrightness = getScreenBrightness(context),
)
} catch (e: Exception) {
Log.w(TAG, "createDeviceEnvInfo", e)
null
}
}
Loading