|
| 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