From 13bc3ae50ea891c20bbc05eb2aaa72d6bec70e62 Mon Sep 17 00:00:00 2001 From: oxmc <67136658+oxmc@users.noreply.github.com> Date: Wed, 17 Jun 2026 06:12:27 -0700 Subject: [PATCH] Fix bugs and add AOSP Android.bp - gen-db.py: fix DB output path (assets/ not res/raw/) - DeviceName: use Build.DEVICE for DB lookup, Build.PRODUCT for AltDeviceNames - DeviceName: fix capitalize() for API 36 (replaceFirstChar) - DBHelper: add 24h rate-limit on update checks via SharedPreferences - DBHelper: make DB download atomic (write to .tmp then rename) - DeviceInfo: wrap getCurrentDeviceInfo() in withContext(Dispatchers.IO) - DeviceInfo: add 30-day SharedPreferences cache for device image URLs - AltDeviceNames: rewrite with Entry class + byModel index for O(1) lookup - Android.bp: add android_library module for AOSP source build --- Android.bp | 14 ++ gen-db.py | 2 +- .../oxmc/androiddeviceinfo/AltDeviceNames.kt | 196 ++++++++---------- .../dev/oxmc/androiddeviceinfo/DBHelper.kt | 22 +- .../dev/oxmc/androiddeviceinfo/DeviceInfo.kt | 57 ++--- .../dev/oxmc/androiddeviceinfo/DeviceName.kt | 8 +- 6 files changed, 145 insertions(+), 154 deletions(-) create mode 100644 Android.bp diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..7250d37 --- /dev/null +++ b/Android.bp @@ -0,0 +1,14 @@ +android_library { + name: "android-device-info", + srcs: ["library/src/main/java/**/*.kt"], + asset_dirs: ["library/src/main/assets"], + sdk_version: "current", + min_sdk_version: "36", + kotlincflags: ["-Xjvm-default=all"], + static_libs: [ + "kotlinx-coroutines-android", + "androidx.annotation_annotation", + "androidx.core_core-ktx", + ], + visibility: ["//visibility:public"], +} diff --git a/gen-db.py b/gen-db.py index 5c1698a..39756ba 100644 --- a/gen-db.py +++ b/gen-db.py @@ -7,7 +7,7 @@ from io import StringIO # Constants CSV_URL = "https://storage.googleapis.com/play_public/supported_devices.csv" -DB_PATH = "library/src/main/res/raw/android-devices.db" +DB_PATH = "library/src/main/assets/android_devices.db" ZIP_PATH = "database/android-devices.zip" # Ensure output directory exists diff --git a/library/src/main/java/dev/oxmc/androiddeviceinfo/AltDeviceNames.kt b/library/src/main/java/dev/oxmc/androiddeviceinfo/AltDeviceNames.kt index aeee564..60a822b 100644 --- a/library/src/main/java/dev/oxmc/androiddeviceinfo/AltDeviceNames.kt +++ b/library/src/main/java/dev/oxmc/androiddeviceinfo/AltDeviceNames.kt @@ -19,135 +19,101 @@ package dev.oxmc.androiddeviceinfo object AltDeviceNames { /** - * A map of device identifiers to their alternate names. - * The key is a Triple containing: - * - List of model names - * - List of manufacturer names - * - List of codenames (can be empty) - * - * The value is the alternate name for the device. + * Represents a consumer device product, which may have multiple model numbers, manufacturer + * names (e.g. rebrands), and codenames (Build.PRODUCT values). Empty codenames means the + * entry matches regardless of codename. */ - private val deviceNames: Map, List, List>, String> = mapOf( + private class Entry( + models: List, + manufacturers: List, + codenames: List = emptyList(), + val name: String + ) { + private val modelSet = models.mapTo(HashSet()) { it.uppercase() } + private val mfrSet = manufacturers.mapTo(HashSet()) { it.uppercase() } + // empty = match any codename; non-empty = must match one of these + private val codenameSet = codenames.filterNot { it.isEmpty() }.mapTo(HashSet()) { it.uppercase() } + val models: List = models + + fun matches(model: String, manufacturer: String, codename: String): Boolean = + model.uppercase() in modelSet && + manufacturer.uppercase() in mfrSet && + (codenameSet.isEmpty() || codename.uppercase() in codenameSet) + } + + private val entries = listOf( // Meta / Oculus / Facebook - Triple(listOf("Meta Quest 3"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest 3", - Triple(listOf("Meta Quest Pro"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest Pro", - Triple(listOf("Meta Quest 2"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest 2", - Triple(listOf("Meta Quest"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest", + Entry(listOf("Meta Quest 3"), listOf("Meta", "oculus"), name = "Meta Quest 3"), + Entry(listOf("Meta Quest Pro"), listOf("Meta", "oculus"), name = "Meta Quest Pro"), + Entry(listOf("Meta Quest 2"), listOf("Meta", "oculus"), name = "Meta Quest 2"), + Entry(listOf("Meta Quest"), listOf("Meta", "oculus"), name = "Meta Quest"), // ZTE - Triple(listOf("Z6251"), listOf("ZTE"), listOf("Z6251")) to "ZMax 11", + Entry(listOf("Z6251"), listOf("ZTE"), listOf("Z6251"), "ZMax 11"), - // OnePlus - Triple(listOf("CPH2195"), listOf("Oppo"), listOf("")) to "OPPO A54 5G", - Triple(listOf("OP595DL1", "CPH2583"), listOf("Oppo"), listOf("")) to "OnePlus 12", + // OnePlus / Oppo + Entry(listOf("CPH2195"), listOf("Oppo"), name = "OPPO A54 5G"), + Entry(listOf("OP595DL1", "CPH2583"), listOf("Oppo"), name = "OnePlus 12"), - // Emulators/sdk - Triple(listOf("sdk_gphone_x86"), listOf("Google"), listOf("")) to "Android SDK (x86)", - Triple(listOf("sdk_gphone_x86_64"), listOf("Google"), listOf("")) to "Android SDK (x86_64)", - Triple(listOf("sdk_gphone_arm64"), listOf("Google"), listOf("")) to "Android SDK (arm64)", - Triple(listOf("sdk_gphone_arm"), listOf("Google"), listOf("")) to "Android SDK (arm)", + // Emulators + Entry(listOf("sdk_gphone_x86"), listOf("Google"), name = "Android SDK (x86)"), + Entry(listOf("sdk_gphone_x86_64"), listOf("Google"), name = "Android SDK (x86_64)"), + Entry(listOf("sdk_gphone_arm64"), listOf("Google"), name = "Android SDK (arm64)"), + Entry(listOf("sdk_gphone_arm"), listOf("Google"), name = "Android SDK (arm)"), // Google - Triple(listOf("Pixel 6"), listOf("Google"), listOf("")) to "Pixel 6", + Entry(listOf("Pixel 6"), listOf("Google"), name = "Pixel 6"), // Motorola - Triple(listOf("moto g power (2021)"), listOf("Motorola"), listOf("")) to "Moto G Power (2021)", - Triple(listOf("XT2163-4"), listOf("Motorola"), listOf("")) to "Moto G Pure", + Entry(listOf("moto g power (2021)"), listOf("Motorola"), name = "Moto G Power (2021)"), + Entry(listOf("XT2163-4"), listOf("Motorola"), name = "Moto G Pure"), // LG - Triple(listOf("LGLS675", "LS675"), listOf("LG"), listOf("LG M1", "")) to "LG Tribute 5", + Entry(listOf("LGLS675", "LS675"), listOf("LG"), listOf("LG M1"), "LG Tribute 5"), - // Samsung (some with empty codenames) - Triple(listOf("SM-S928U"), listOf("SAMSUNG"), listOf("e3qsqw")) to "Galaxy S24 Ultra", - Triple(listOf("SM-S901U"), listOf("SAMSUNG"), listOf("r0qsqw")) to "Samsung Galaxy S22", - Triple(listOf("SM-A526U"), listOf("SAMSUNG"), listOf("")) to "Galaxy A52 5G (US)", - Triple(listOf("SM-A326U"), listOf("SAMSUNG"), listOf("")) to "Galaxy A32 5G (US)", - Triple(listOf("SM-G998U"), listOf("SAMSUNG"), listOf("")) to "Galaxy S21 Ultra 5G (US)", - Triple(listOf("SM-G998W"), listOf("SAMSUNG"), listOf("p3qcsx")) to "Galaxy S21 Ultra 5G (CA)", - Triple(listOf("SM-G991U"), listOf("SAMSUNG"), listOf("")) to "Galaxy S21 5G", - Triple(listOf("SM-A125F"), listOf("SAMSUNG"), listOf("")) to "Galaxy A12", - Triple(listOf("SM-T290"), listOf("SAMSUNG"), listOf("gtowifixx")) to "Galaxy Tab A8", - Triple(listOf("SM-G780F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S20 FE", - Triple(listOf("SM-N986U"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 20 Ultra 5G", - Triple(listOf("SM-N981U"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 20 5G", - Triple(listOf("A013G"), listOf("SAMSUNG"), listOf("")) to "Galaxy A01 Core", - Triple(listOf("SM-A715F"), listOf("SAMSUNG"), listOf("")) to "Galaxy A71", - Triple(listOf("SM-A515F"), listOf("SAMSUNG"), listOf("")) to "Galaxy A51", - Triple(listOf("SM-G975F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S10+", - Triple(listOf("SM-G973F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S10", - Triple(listOf("SM-G970F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S10e", - Triple(listOf("SM-N960F"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 9", - Triple(listOf("SM-G965F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S9+", - Triple(listOf("SM-G960F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S9", - Triple(listOf("SM-N950F"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 8", - Triple(listOf("SM-J730F"), listOf("SAMSUNG"), listOf("")) to "Galaxy J7 (2017)", - Triple(listOf("SM-J530F"), listOf("SAMSUNG"), listOf("")) to "Galaxy J5 (2017)", - Triple(listOf("SM-J330F"), listOf("SAMSUNG"), listOf("")) to "Galaxy J3 (2017)", - Triple(listOf("SM-G955F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S8+", - Triple(listOf("SM-G950F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S8" + // Samsung + Entry(listOf("SM-S928U"), listOf("SAMSUNG"), listOf("e3qsqw"), "Galaxy S24 Ultra"), + Entry(listOf("SM-S901U"), listOf("SAMSUNG"), listOf("r0qsqw"), "Samsung Galaxy S22"), + Entry(listOf("SM-A526U"), listOf("SAMSUNG"), name = "Galaxy A52 5G (US)"), + Entry(listOf("SM-A326U"), listOf("SAMSUNG"), name = "Galaxy A32 5G (US)"), + Entry(listOf("SM-G998U"), listOf("SAMSUNG"), name = "Galaxy S21 Ultra 5G (US)"), + Entry(listOf("SM-G998W"), listOf("SAMSUNG"), listOf("p3qcsx"), "Galaxy S21 Ultra 5G (CA)"), + Entry(listOf("SM-G991U"), listOf("SAMSUNG"), name = "Galaxy S21 5G"), + Entry(listOf("SM-A125F"), listOf("SAMSUNG"), name = "Galaxy A12"), + Entry(listOf("SM-T290"), listOf("SAMSUNG"), listOf("gtowifixx"), "Galaxy Tab A8"), + Entry(listOf("SM-G780F"), listOf("SAMSUNG"), name = "Galaxy S20 FE"), + Entry(listOf("SM-N986U"), listOf("SAMSUNG"), name = "Galaxy Note 20 Ultra 5G"), + Entry(listOf("SM-N981U"), listOf("SAMSUNG"), name = "Galaxy Note 20 5G"), + Entry(listOf("A013G"), listOf("SAMSUNG"), name = "Galaxy A01 Core"), + Entry(listOf("SM-A715F"), listOf("SAMSUNG"), name = "Galaxy A71"), + Entry(listOf("SM-A515F"), listOf("SAMSUNG"), name = "Galaxy A51"), + Entry(listOf("SM-G975F"), listOf("SAMSUNG"), name = "Galaxy S10+"), + Entry(listOf("SM-G973F"), listOf("SAMSUNG"), name = "Galaxy S10"), + Entry(listOf("SM-G970F"), listOf("SAMSUNG"), name = "Galaxy S10e"), + Entry(listOf("SM-N960F"), listOf("SAMSUNG"), name = "Galaxy Note 9"), + Entry(listOf("SM-G965F"), listOf("SAMSUNG"), name = "Galaxy S9+"), + Entry(listOf("SM-G960F"), listOf("SAMSUNG"), name = "Galaxy S9"), + Entry(listOf("SM-N950F"), listOf("SAMSUNG"), name = "Galaxy Note 8"), + Entry(listOf("SM-J730F"), listOf("SAMSUNG"), name = "Galaxy J7 (2017)"), + Entry(listOf("SM-J530F"), listOf("SAMSUNG"), name = "Galaxy J5 (2017)"), + Entry(listOf("SM-J330F"), listOf("SAMSUNG"), name = "Galaxy J3 (2017)"), + Entry(listOf("SM-G955F"), listOf("SAMSUNG"), name = "Galaxy S8+"), + Entry(listOf("SM-G950F"), listOf("SAMSUNG"), name = "Galaxy S8"), ) - /** - * Returns an alternate name for the device based on its model, manufacturer, and codename. - * If no alternate name is found, returns null. - * - * @param model The model of the device. - * @param manufacturer The manufacturer of the device. - * @param codename The codename of the device (can be empty). - * @return The alternate name or null if not found. - */ - private fun getAlternateName(model: String, manufacturer: String, codename: String): String? { - return deviceNames.entries.find { (key, _) -> - model in key.first && - manufacturer in key.second && - ((key.third.isEmpty() && codename.isBlank()) || (key.third.isNotEmpty() && codename in key.third)) - }?.value - } - - /** - * Returns an alternate name for the device based on its model and manufacturer. - * If no alternate name is found, returns null. - * - * @param model The model of the device. - * @param manufacturer The manufacturer of the device. - * @param codename Optional codename of the device (can be null). - * @return The alternate name or null if not found. - */ - private fun getAlternateNameFlexible(model: String, manufacturer: String, codename: String? = null): String? { - return deviceNames.entries.find { (key, _) -> - model in key.first && - manufacturer in key.second && - ((key.third.isEmpty() && codename.isNullOrBlank()) || (key.third.isNotEmpty() && codename != null && codename in key.third)) - }?.value - } - - /** - * Returns an alternate name for the device based on its model and manufacturer. - * If no alternate name is found, returns null. - * - * @param model The model of the device. - * @param manufacturer The manufacturer of the device. - * @return The alternate name or null if not found. - */ - fun getAlternateNameOrDefault(model: String, manufacturer: String, codename: String, default: String): String { - return if (codename.isBlank()) { - getAlternateNameFlexible(model, manufacturer) - } else { - getAlternateName(model, manufacturer, codename) - } ?: default - } - - /** - * Checks if the device identifiers are present in the alternate names map. - * - * @param model The model of the device. - * @param manufacturer The manufacturer of the device. - * @param codename The codename of the device (can be empty). - * @return True if the identifiers are found, false otherwise. - */ - fun containsIdentifier(model: String, manufacturer: String, codename: String): Boolean { - return deviceNames.entries.any { (key, _) -> - model in key.first && manufacturer in key.second && codename in key.third + // Index built at class init: model (uppercase) → entries that contain that model. + // O(1) first-level lookup; secondary filter is O(small) since model collisions are rare. + private val byModel: Map> = buildMap> { + for (entry in entries) { + for (model in entry.models) { + getOrPut(model.uppercase()) { mutableListOf() }.add(entry) + } } } -} \ No newline at end of file + + fun getAlternateNameOrDefault(model: String, manufacturer: String, codename: String, default: String): String = + byModel[model.uppercase()]?.find { it.matches(model, manufacturer, codename) }?.name ?: default + + fun containsIdentifier(model: String, manufacturer: String, codename: String): Boolean = + byModel[model.uppercase()]?.any { it.matches(model, manufacturer, codename) } ?: false +} diff --git a/library/src/main/java/dev/oxmc/androiddeviceinfo/DBHelper.kt b/library/src/main/java/dev/oxmc/androiddeviceinfo/DBHelper.kt index 7690ad0..2974817 100644 --- a/library/src/main/java/dev/oxmc/androiddeviceinfo/DBHelper.kt +++ b/library/src/main/java/dev/oxmc/androiddeviceinfo/DBHelper.kt @@ -180,6 +180,10 @@ class DeviceDatabase(context: Context, private val autoUpdateDB: Boolean = true) } private fun checkForUpdates() { + val prefs = context.getSharedPreferences("device_db_prefs", Context.MODE_PRIVATE) + val lastCheck = prefs.getLong("last_update_check", 0L) + if (System.currentTimeMillis() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return + prefs.edit().putLong("last_update_check", System.currentTimeMillis()).apply() CoroutineScope(Dispatchers.IO).launch { try { val url = URL(UPDATE_URL) @@ -203,22 +207,21 @@ class DeviceDatabase(context: Context, private val autoUpdateDB: Boolean = true) } } } catch (e: Exception) { - // Handle error during update check, e.g., log it e.printStackTrace() } } } private suspend fun downloadAndUpdateDatabase(downloadUrl: String) = withContext(Dispatchers.IO) { + val tempFile = File(file.parent, "$NAME.tmp") try { - val url = URL(downloadUrl) - val connection = url.openConnection() as HttpURLConnection + val connection = URL(downloadUrl).openConnection() as HttpURLConnection connection.connect() - context.deleteDatabase(NAME) // Delete old database - val outputStream = FileOutputStream(file) - connection.inputStream.copyTo(outputStream) - outputStream.close() + FileOutputStream(tempFile).use { connection.inputStream.copyTo(it) } + context.deleteDatabase(NAME) + tempFile.renameTo(file) } catch (e: IOException) { + tempFile.delete() throw SQLException("Error downloading or updating $NAME database", e) } } @@ -260,8 +263,9 @@ class DeviceDatabase(context: Context, private val autoUpdateDB: Boolean = true) private const val COLUMN_CODENAME = "codename" private const val COLUMN_MODEL = "model" private const val NAME = "android_devices.db" - private const val VERSION_CODE = 2 // Integer, increment for schema changes - private const val VERSION_NAME = "1.0.1" // SemVer for content updates + private const val VERSION_CODE = 2 + private const val VERSION_NAME = "1.0.1" private const val UPDATE_URL = "https://cdn.oxmc.me/android-devicenames/latest.json" + private const val UPDATE_CHECK_INTERVAL_MS = 86_400_000L // 24 hours } } \ No newline at end of file diff --git a/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceInfo.kt b/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceInfo.kt index 7514f55..cfc1952 100755 --- a/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceInfo.kt +++ b/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceInfo.kt @@ -29,12 +29,12 @@ import java.net.URL import java.net.URLEncoder object DeviceInfo { - // API base URL and endpoints - var api_url_base = "https://cdn.oxmc.me/api"; - var api_device = "device-info"; - var api_deviceSearch = "${api_device}/search-device"; - var api_reportDevice = "${api_device}/report-device-info"; - var api_version = "v2"; + var api_url_base = "https://cdn.oxmc.me/api" + var api_device = "device-info" + var api_deviceSearch = "${api_device}/search-device" + var api_reportDevice = "${api_device}/report-device-info" + var api_version = "v2" + private const val IMAGE_CACHE_MS = 2_592_000_000L // 30 days /** * Get device information including name, manufacturer, model, codename, and image URL. @@ -43,8 +43,9 @@ object DeviceInfo { * @return An array containing device name, manufacturer, model, codename, and image URL. */ suspend fun getDeviceInfo(context: Context): Array { - // Get basic device information - val deviceInfo = AndroidDeviceInfo.getCurrentDeviceInfo(context) + val deviceInfo = withContext(Dispatchers.IO) { + AndroidDeviceInfo.getCurrentDeviceInfo(context) + } // Set all the device information val deviceName = deviceInfo.name ?: "Unknown" @@ -101,34 +102,35 @@ object DeviceInfo { * @return The URL of the device image or null if not found. */ suspend fun getDeviceImage(context: Context, codename: String, model: String, manufacturer: String): String? { + val prefs = context.getSharedPreferences("device_images", Context.MODE_PRIVATE) + val cacheKey = "$codename:$model" + prefs.getString(cacheKey, null)?.let { cached -> + try { + val json = JSONObject(cached) + if (System.currentTimeMillis() - json.getLong("ts") < IMAGE_CACHE_MS) { + val url = json.optString("url", "") + return if (url.isEmpty()) null else url + } + } catch (_: Exception) {} + } + val baseUrl = "${api_url_base}/${api_deviceSearch}/${api_version}" - val queryParams = mutableListOf() + val fullUrl = "$baseUrl?codename=${URLEncoder.encode(codename, "UTF-8")}" + + "&model=${URLEncoder.encode(model, "UTF-8")}" + + "&manufacturer=${URLEncoder.encode(manufacturer, "UTF-8")}" - // Encode parameters to ensure they are URL-safe - queryParams.add("codename=${URLEncoder.encode(codename, "UTF-8")}") - queryParams.add("model=${URLEncoder.encode(model, "UTF-8")}") - queryParams.add("manufacturer=${URLEncoder.encode(manufacturer, "UTF-8")}") - - // Construct the full URL with query parameters - val fullUrl = "$baseUrl?${queryParams.joinToString("&")}" - - // Get the device image from the API var deviceImage: String? = null withContext(Dispatchers.IO) { var connection: HttpURLConnection? = null try { - val url = URL(fullUrl) - connection = url.openConnection() as HttpURLConnection + connection = URL(fullUrl).openConnection() as HttpURLConnection connection.requestMethod = "GET" connection.connectTimeout = 5000 connection.readTimeout = 5000 - - // Check the response code if (connection.responseCode == HttpURLConnection.HTTP_OK) { val response = connection.inputStream.bufferedReader().use(BufferedReader::readText) val jsonObject = JSONObject(response) - deviceImage = jsonObject.optString("image") - if (deviceImage.isEmpty()) deviceImage = null + deviceImage = jsonObject.optString("image").ifEmpty { null } } else { println("HTTP Error: ${connection.responseCode} ${connection.responseMessage} for URL: $fullUrl") } @@ -138,7 +140,12 @@ object DeviceInfo { connection?.disconnect() } } - + try { + val cacheJson = JSONObject() + cacheJson.put("url", deviceImage ?: "") + cacheJson.put("ts", System.currentTimeMillis()) + prefs.edit().putString(cacheKey, cacheJson.toString()).apply() + } catch (_: Exception) {} return deviceImage } diff --git a/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceName.kt b/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceName.kt index dfa53f5..21cd75f 100644 --- a/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceName.kt +++ b/library/src/main/java/dev/oxmc/androiddeviceinfo/DeviceName.kt @@ -40,7 +40,7 @@ object AndroidDeviceInfo { @WorkerThread fun getCurrentDeviceInfo(context: Context?): DeviceInfo { - val codename = Build.PRODUCT + val codename = Build.DEVICE val model = Build.MODEL val cachedInfo = getDeviceInfoFromCache(context, codename, model) @@ -67,8 +67,8 @@ object AndroidDeviceInfo { e.printStackTrace() } - // Attempt to get the alternate name of the device - val altName = AltDeviceNames.getAlternateNameOrDefault(capitalize(model), capitalize(manufacturer), codename, codename) + val product = Build.PRODUCT + val altName = AltDeviceNames.getAlternateNameOrDefault(capitalize(model), capitalize(manufacturer), product, product) val finalDeviceName: String = if (!TextUtils.isEmpty(altName)) { altName } else { @@ -125,7 +125,7 @@ object AndroidDeviceInfo { if (TextUtils.isEmpty(str)) { return str } - return str.uppercase() + return str.replaceFirstChar { it.uppercaseChar() } } class DeviceInfo @JvmOverloads constructor(