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
This commit is contained in:
oxmc
2026-06-17 06:12:27 -07:00
parent 22177d3be4
commit 13bc3ae50e
6 changed files with 145 additions and 154 deletions
+14
View File
@@ -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"],
}
+1 -1
View File
@@ -7,7 +7,7 @@ from io import StringIO
# Constants # Constants
CSV_URL = "https://storage.googleapis.com/play_public/supported_devices.csv" 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" ZIP_PATH = "database/android-devices.zip"
# Ensure output directory exists # Ensure output directory exists
@@ -19,135 +19,101 @@ package dev.oxmc.androiddeviceinfo
object AltDeviceNames { object AltDeviceNames {
/** /**
* A map of device identifiers to their alternate names. * Represents a consumer device product, which may have multiple model numbers, manufacturer
* The key is a Triple containing: * names (e.g. rebrands), and codenames (Build.PRODUCT values). Empty codenames means the
* - List of model names * entry matches regardless of codename.
* - List of manufacturer names
* - List of codenames (can be empty)
*
* The value is the alternate name for the device.
*/ */
private val deviceNames: Map<Triple<List<String>, List<String>, List<String>>, String> = mapOf( private class Entry(
models: List<String>,
manufacturers: List<String>,
codenames: List<String> = 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<String> = 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 // Meta / Oculus / Facebook
Triple(listOf("Meta Quest 3"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest 3", Entry(listOf("Meta Quest 3"), listOf("Meta", "oculus"), name = "Meta Quest 3"),
Triple(listOf("Meta Quest Pro"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest Pro", Entry(listOf("Meta Quest Pro"), listOf("Meta", "oculus"), name = "Meta Quest Pro"),
Triple(listOf("Meta Quest 2"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest 2", Entry(listOf("Meta Quest 2"), listOf("Meta", "oculus"), name = "Meta Quest 2"),
Triple(listOf("Meta Quest"), listOf("Meta", "oculus"), listOf("")) to "Meta Quest", Entry(listOf("Meta Quest"), listOf("Meta", "oculus"), name = "Meta Quest"),
// ZTE // ZTE
Triple(listOf("Z6251"), listOf("ZTE"), listOf("Z6251")) to "ZMax 11", Entry(listOf("Z6251"), listOf("ZTE"), listOf("Z6251"), "ZMax 11"),
// OnePlus // OnePlus / Oppo
Triple(listOf("CPH2195"), listOf("Oppo"), listOf("")) to "OPPO A54 5G", Entry(listOf("CPH2195"), listOf("Oppo"), name = "OPPO A54 5G"),
Triple(listOf("OP595DL1", "CPH2583"), listOf("Oppo"), listOf("")) to "OnePlus 12", Entry(listOf("OP595DL1", "CPH2583"), listOf("Oppo"), name = "OnePlus 12"),
// Emulators/sdk // Emulators
Triple(listOf("sdk_gphone_x86"), listOf("Google"), listOf("")) to "Android SDK (x86)", Entry(listOf("sdk_gphone_x86"), listOf("Google"), name = "Android SDK (x86)"),
Triple(listOf("sdk_gphone_x86_64"), listOf("Google"), listOf("")) to "Android SDK (x86_64)", Entry(listOf("sdk_gphone_x86_64"), listOf("Google"), name = "Android SDK (x86_64)"),
Triple(listOf("sdk_gphone_arm64"), listOf("Google"), listOf("")) to "Android SDK (arm64)", Entry(listOf("sdk_gphone_arm64"), listOf("Google"), name = "Android SDK (arm64)"),
Triple(listOf("sdk_gphone_arm"), listOf("Google"), listOf("")) to "Android SDK (arm)", Entry(listOf("sdk_gphone_arm"), listOf("Google"), name = "Android SDK (arm)"),
// Google // Google
Triple(listOf("Pixel 6"), listOf("Google"), listOf("")) to "Pixel 6", Entry(listOf("Pixel 6"), listOf("Google"), name = "Pixel 6"),
// Motorola // Motorola
Triple(listOf("moto g power (2021)"), listOf("Motorola"), listOf("")) to "Moto G Power (2021)", Entry(listOf("moto g power (2021)"), listOf("Motorola"), name = "Moto G Power (2021)"),
Triple(listOf("XT2163-4"), listOf("Motorola"), listOf("")) to "Moto G Pure", Entry(listOf("XT2163-4"), listOf("Motorola"), name = "Moto G Pure"),
// LG // 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) // Samsung
Triple(listOf("SM-S928U"), listOf("SAMSUNG"), listOf("e3qsqw")) to "Galaxy S24 Ultra", Entry(listOf("SM-S928U"), listOf("SAMSUNG"), listOf("e3qsqw"), "Galaxy S24 Ultra"),
Triple(listOf("SM-S901U"), listOf("SAMSUNG"), listOf("r0qsqw")) to "Samsung Galaxy S22", Entry(listOf("SM-S901U"), listOf("SAMSUNG"), listOf("r0qsqw"), "Samsung Galaxy S22"),
Triple(listOf("SM-A526U"), listOf("SAMSUNG"), listOf("")) to "Galaxy A52 5G (US)", Entry(listOf("SM-A526U"), listOf("SAMSUNG"), name = "Galaxy A52 5G (US)"),
Triple(listOf("SM-A326U"), listOf("SAMSUNG"), listOf("")) to "Galaxy A32 5G (US)", Entry(listOf("SM-A326U"), listOf("SAMSUNG"), name = "Galaxy A32 5G (US)"),
Triple(listOf("SM-G998U"), listOf("SAMSUNG"), listOf("")) to "Galaxy S21 Ultra 5G (US)", Entry(listOf("SM-G998U"), listOf("SAMSUNG"), name = "Galaxy S21 Ultra 5G (US)"),
Triple(listOf("SM-G998W"), listOf("SAMSUNG"), listOf("p3qcsx")) to "Galaxy S21 Ultra 5G (CA)", Entry(listOf("SM-G998W"), listOf("SAMSUNG"), listOf("p3qcsx"), "Galaxy S21 Ultra 5G (CA)"),
Triple(listOf("SM-G991U"), listOf("SAMSUNG"), listOf("")) to "Galaxy S21 5G", Entry(listOf("SM-G991U"), listOf("SAMSUNG"), name = "Galaxy S21 5G"),
Triple(listOf("SM-A125F"), listOf("SAMSUNG"), listOf("")) to "Galaxy A12", Entry(listOf("SM-A125F"), listOf("SAMSUNG"), name = "Galaxy A12"),
Triple(listOf("SM-T290"), listOf("SAMSUNG"), listOf("gtowifixx")) to "Galaxy Tab A8", Entry(listOf("SM-T290"), listOf("SAMSUNG"), listOf("gtowifixx"), "Galaxy Tab A8"),
Triple(listOf("SM-G780F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S20 FE", Entry(listOf("SM-G780F"), listOf("SAMSUNG"), name = "Galaxy S20 FE"),
Triple(listOf("SM-N986U"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 20 Ultra 5G", Entry(listOf("SM-N986U"), listOf("SAMSUNG"), name = "Galaxy Note 20 Ultra 5G"),
Triple(listOf("SM-N981U"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 20 5G", Entry(listOf("SM-N981U"), listOf("SAMSUNG"), name = "Galaxy Note 20 5G"),
Triple(listOf("A013G"), listOf("SAMSUNG"), listOf("")) to "Galaxy A01 Core", Entry(listOf("A013G"), listOf("SAMSUNG"), name = "Galaxy A01 Core"),
Triple(listOf("SM-A715F"), listOf("SAMSUNG"), listOf("")) to "Galaxy A71", Entry(listOf("SM-A715F"), listOf("SAMSUNG"), name = "Galaxy A71"),
Triple(listOf("SM-A515F"), listOf("SAMSUNG"), listOf("")) to "Galaxy A51", Entry(listOf("SM-A515F"), listOf("SAMSUNG"), name = "Galaxy A51"),
Triple(listOf("SM-G975F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S10+", Entry(listOf("SM-G975F"), listOf("SAMSUNG"), name = "Galaxy S10+"),
Triple(listOf("SM-G973F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S10", Entry(listOf("SM-G973F"), listOf("SAMSUNG"), name = "Galaxy S10"),
Triple(listOf("SM-G970F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S10e", Entry(listOf("SM-G970F"), listOf("SAMSUNG"), name = "Galaxy S10e"),
Triple(listOf("SM-N960F"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 9", Entry(listOf("SM-N960F"), listOf("SAMSUNG"), name = "Galaxy Note 9"),
Triple(listOf("SM-G965F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S9+", Entry(listOf("SM-G965F"), listOf("SAMSUNG"), name = "Galaxy S9+"),
Triple(listOf("SM-G960F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S9", Entry(listOf("SM-G960F"), listOf("SAMSUNG"), name = "Galaxy S9"),
Triple(listOf("SM-N950F"), listOf("SAMSUNG"), listOf("")) to "Galaxy Note 8", Entry(listOf("SM-N950F"), listOf("SAMSUNG"), name = "Galaxy Note 8"),
Triple(listOf("SM-J730F"), listOf("SAMSUNG"), listOf("")) to "Galaxy J7 (2017)", Entry(listOf("SM-J730F"), listOf("SAMSUNG"), name = "Galaxy J7 (2017)"),
Triple(listOf("SM-J530F"), listOf("SAMSUNG"), listOf("")) to "Galaxy J5 (2017)", Entry(listOf("SM-J530F"), listOf("SAMSUNG"), name = "Galaxy J5 (2017)"),
Triple(listOf("SM-J330F"), listOf("SAMSUNG"), listOf("")) to "Galaxy J3 (2017)", Entry(listOf("SM-J330F"), listOf("SAMSUNG"), name = "Galaxy J3 (2017)"),
Triple(listOf("SM-G955F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S8+", Entry(listOf("SM-G955F"), listOf("SAMSUNG"), name = "Galaxy S8+"),
Triple(listOf("SM-G950F"), listOf("SAMSUNG"), listOf("")) to "Galaxy S8" Entry(listOf("SM-G950F"), listOf("SAMSUNG"), name = "Galaxy S8"),
) )
/** // Index built at class init: model (uppercase) → entries that contain that model.
* Returns an alternate name for the device based on its model, manufacturer, and codename. // O(1) first-level lookup; secondary filter is O(small) since model collisions are rare.
* If no alternate name is found, returns null. private val byModel: Map<String, List<Entry>> = buildMap<String, MutableList<Entry>> {
* for (entry in entries) {
* @param model The model of the device. for (model in entry.models) {
* @param manufacturer The manufacturer of the device. getOrPut(model.uppercase()) { mutableListOf() }.add(entry)
* @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
} }
} }
}
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
}
@@ -180,6 +180,10 @@ class DeviceDatabase(context: Context, private val autoUpdateDB: Boolean = true)
} }
private fun checkForUpdates() { 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 { CoroutineScope(Dispatchers.IO).launch {
try { try {
val url = URL(UPDATE_URL) val url = URL(UPDATE_URL)
@@ -203,22 +207,21 @@ class DeviceDatabase(context: Context, private val autoUpdateDB: Boolean = true)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
// Handle error during update check, e.g., log it
e.printStackTrace() e.printStackTrace()
} }
} }
} }
private suspend fun downloadAndUpdateDatabase(downloadUrl: String) = withContext(Dispatchers.IO) { private suspend fun downloadAndUpdateDatabase(downloadUrl: String) = withContext(Dispatchers.IO) {
val tempFile = File(file.parent, "$NAME.tmp")
try { try {
val url = URL(downloadUrl) val connection = URL(downloadUrl).openConnection() as HttpURLConnection
val connection = url.openConnection() as HttpURLConnection
connection.connect() connection.connect()
context.deleteDatabase(NAME) // Delete old database FileOutputStream(tempFile).use { connection.inputStream.copyTo(it) }
val outputStream = FileOutputStream(file) context.deleteDatabase(NAME)
connection.inputStream.copyTo(outputStream) tempFile.renameTo(file)
outputStream.close()
} catch (e: IOException) { } catch (e: IOException) {
tempFile.delete()
throw SQLException("Error downloading or updating $NAME database", e) 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_CODENAME = "codename"
private const val COLUMN_MODEL = "model" private const val COLUMN_MODEL = "model"
private const val NAME = "android_devices.db" private const val NAME = "android_devices.db"
private const val VERSION_CODE = 2 // Integer, increment for schema changes private const val VERSION_CODE = 2
private const val VERSION_NAME = "1.0.1" // SemVer for content updates 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_URL = "https://cdn.oxmc.me/android-devicenames/latest.json"
private const val UPDATE_CHECK_INTERVAL_MS = 86_400_000L // 24 hours
} }
} }
@@ -29,12 +29,12 @@ import java.net.URL
import java.net.URLEncoder import java.net.URLEncoder
object DeviceInfo { object DeviceInfo {
// API base URL and endpoints var api_url_base = "https://cdn.oxmc.me/api"
var api_url_base = "https://cdn.oxmc.me/api"; var api_device = "device-info"
var api_device = "device-info"; var api_deviceSearch = "${api_device}/search-device"
var api_deviceSearch = "${api_device}/search-device"; var api_reportDevice = "${api_device}/report-device-info"
var api_reportDevice = "${api_device}/report-device-info"; var api_version = "v2"
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. * 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. * @return An array containing device name, manufacturer, model, codename, and image URL.
*/ */
suspend fun getDeviceInfo(context: Context): Array<String?> { suspend fun getDeviceInfo(context: Context): Array<String?> {
// Get basic device information val deviceInfo = withContext(Dispatchers.IO) {
val deviceInfo = AndroidDeviceInfo.getCurrentDeviceInfo(context) AndroidDeviceInfo.getCurrentDeviceInfo(context)
}
// Set all the device information // Set all the device information
val deviceName = deviceInfo.name ?: "Unknown" val deviceName = deviceInfo.name ?: "Unknown"
@@ -101,34 +102,35 @@ object DeviceInfo {
* @return The URL of the device image or null if not found. * @return The URL of the device image or null if not found.
*/ */
suspend fun getDeviceImage(context: Context, codename: String, model: String, manufacturer: String): String? { 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 baseUrl = "${api_url_base}/${api_deviceSearch}/${api_version}"
val queryParams = mutableListOf<String>() 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 var deviceImage: String? = null
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null var connection: HttpURLConnection? = null
try { try {
val url = URL(fullUrl) connection = URL(fullUrl).openConnection() as HttpURLConnection
connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET" connection.requestMethod = "GET"
connection.connectTimeout = 5000 connection.connectTimeout = 5000
connection.readTimeout = 5000 connection.readTimeout = 5000
// Check the response code
if (connection.responseCode == HttpURLConnection.HTTP_OK) { if (connection.responseCode == HttpURLConnection.HTTP_OK) {
val response = connection.inputStream.bufferedReader().use(BufferedReader::readText) val response = connection.inputStream.bufferedReader().use(BufferedReader::readText)
val jsonObject = JSONObject(response) val jsonObject = JSONObject(response)
deviceImage = jsonObject.optString("image") deviceImage = jsonObject.optString("image").ifEmpty { null }
if (deviceImage.isEmpty()) deviceImage = null
} else { } else {
println("HTTP Error: ${connection.responseCode} ${connection.responseMessage} for URL: $fullUrl") println("HTTP Error: ${connection.responseCode} ${connection.responseMessage} for URL: $fullUrl")
} }
@@ -138,7 +140,12 @@ object DeviceInfo {
connection?.disconnect() 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 return deviceImage
} }
@@ -40,7 +40,7 @@ object AndroidDeviceInfo {
@WorkerThread @WorkerThread
fun getCurrentDeviceInfo(context: Context?): DeviceInfo { fun getCurrentDeviceInfo(context: Context?): DeviceInfo {
val codename = Build.PRODUCT val codename = Build.DEVICE
val model = Build.MODEL val model = Build.MODEL
val cachedInfo = getDeviceInfoFromCache(context, codename, model) val cachedInfo = getDeviceInfoFromCache(context, codename, model)
@@ -67,8 +67,8 @@ object AndroidDeviceInfo {
e.printStackTrace() e.printStackTrace()
} }
// Attempt to get the alternate name of the device val product = Build.PRODUCT
val altName = AltDeviceNames.getAlternateNameOrDefault(capitalize(model), capitalize(manufacturer), codename, codename) val altName = AltDeviceNames.getAlternateNameOrDefault(capitalize(model), capitalize(manufacturer), product, product)
val finalDeviceName: String = if (!TextUtils.isEmpty(altName)) { val finalDeviceName: String = if (!TextUtils.isEmpty(altName)) {
altName altName
} else { } else {
@@ -125,7 +125,7 @@ object AndroidDeviceInfo {
if (TextUtils.isEmpty(str)) { if (TextUtils.isEmpty(str)) {
return str return str
} }
return str.uppercase() return str.replaceFirstChar { it.uppercaseChar() }
} }
class DeviceInfo @JvmOverloads constructor( class DeviceInfo @JvmOverloads constructor(