first upload

This commit is contained in:
2025-08-06 17:26:20 -07:00
commit 354f1a6f96
45 changed files with 1965 additions and 0 deletions

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Ignore Gradle and build output
.gradle/
build/
**/build/
.cxx/
.externalNativeBuild/
# Ignore database builds
library/src/main/res/raw/android_devices.db
database/android-devices.zip
# Ignore local configuration files
local.properties
/local.properties
.DS_Store
# Ignore IntelliJ/Android Studio project files
*.iml
.idea/
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
# Ignore Android Studio captures
captures/
# Ignore misc
*.apk
*.ap_
*.log
*.keystore

6
build.gradle.kts Normal file
View File

@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
}

1
demo/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

42
demo/build.gradle.kts Normal file
View File

@@ -0,0 +1,42 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "dev.oxmc.androiddeviceinfo_demo"
compileSdk = 36
defaultConfig {
applicationId = "dev.oxmc.androiddeviceinfo_demo"
minSdk = 21
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.coil)
implementation(project(":library"))
}

21
demo/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="false"
android:theme="@style/Theme.AndroidDeviceInfo">
<activity android:name="dev.oxmc.androiddeviceinfo_demo.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,101 @@
package dev.oxmc.androiddeviceinfo_demo
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import coil.load
import dev.oxmc.androiddevicenames.DeviceInfo
import dev.oxmc.androiddevicenames.AndroidInfo
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var layout: LinearLayout
private lateinit var infoView: TextView
private lateinit var imageView: ImageView
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.title = "Android Device Info Demo"
// Main container layout
val rootLayout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
// Add top padding to avoid action bar (56dp is standard action bar height)
setPadding(0, 56, 0, 0)
}
// Content layout that will be centered
layout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
// This centers the content vertically
gravity = Gravity.CENTER
}
// Internal padding for the content
setPadding(32, 32, 32, 32)
}
infoView = TextView(this).apply {
textSize = 16f
setPadding(0, 16, 0, 16)
}
imageView = ImageView(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
600
).apply {
// Add margins around the image
setMargins(0, 16, 0, 16)
}
scaleType = ImageView.ScaleType.FIT_CENTER
adjustViewBounds = true
}
layout.addView(imageView)
layout.addView(infoView)
rootLayout.addView(layout)
setContentView(rootLayout)
lifecycleScope.launch {
try {
val (deviceName, manufacturer, model, codename, imageUrl) =
DeviceInfo.getDeviceInfo(this@MainActivity)
val deviceType = AndroidInfo.getType(this@MainActivity)
infoView.text = """
Device Name: $deviceName
Manufacturer: $manufacturer
Model: $model
Codename: $codename
Device Type: $deviceType
Android Version: ${AndroidInfo.Version.release} (SDK ${AndroidInfo.Version.sdkInt})
Image URL: ${imageUrl ?: "N/A"}
""".trimIndent()
imageUrl?.let { url ->
imageView.load(url) {
crossfade(true)
}
}
} catch (e: Exception) {
infoView.text = "Error loading device info: ${e.message}"
}
}
}
}

View File

@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AndroidDeviceInfo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="21">?attr/colorPrimaryVariant</item>
</style>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<string name="app_name">AndroidDeviceNames</string>
</resources>

View File

@@ -0,0 +1,15 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.AndroidDeviceNames" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="21">?attr/colorPrimaryVariant</item>
</style>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

90
gen-db.py Normal file
View File

@@ -0,0 +1,90 @@
import csv
import os
import sqlite3
import zipfile
import requests
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"
ZIP_PATH = "database/android-devices.zip"
# Ensure output directory exists
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
os.makedirs(os.path.dirname(ZIP_PATH), exist_ok=True)
def download_devices_csv(url=CSV_URL):
print("Downloading CSV from Google Play...")
response = requests.get(url)
response.encoding = "utf-16"
if response.status_code != 200:
raise Exception(f"Failed to fetch CSV. Status code: {response.status_code}")
return response.text
def parse_devices(csv_data):
print("Parsing CSV...")
reader = csv.reader(StringIO(csv_data))
next(reader) # Skip header
devices = []
for row in reader:
if len(row) == 4:
manufacturer, name, codename, model = row
devices.append((manufacturer, name, codename, model))
print(f"Parsed {len(devices)} devices.")
return devices
def create_database(devices):
print("Creating SQLite database...")
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Drop old tables
cursor.execute("DROP TABLE IF EXISTS devices;")
cursor.execute("DROP TABLE IF EXISTS android_metadata;")
# Create android_metadata table
cursor.execute("""
CREATE TABLE android_metadata (
locale TEXT DEFAULT 'en_US'
);
""")
cursor.execute("INSERT INTO android_metadata (locale) VALUES ('en_US');")
# Create devices table
cursor.execute("""
CREATE TABLE devices (
_id INTEGER PRIMARY KEY,
manufacturer TEXT,
name TEXT,
codename TEXT,
model TEXT
);
""")
# Insert all device entries
cursor.executemany("""
INSERT INTO devices (manufacturer, name, codename, model)
VALUES (?, ?, ?, ?);
""", devices)
conn.commit()
conn.close()
print("Database populated successfully.")
def zip_database():
print(f"Zipping database to {ZIP_PATH}...")
with zipfile.ZipFile(ZIP_PATH, 'w', zipfile.ZIP_DEFLATED) as zipf:
zipf.write(DB_PATH, arcname=os.path.basename(DB_PATH))
print("Zip archive created.")
def main():
csv_data = download_devices_csv()
devices = parse_devices(csv_data)
create_database(devices)
zip_database()
print("Done.")
if __name__ == "__main__":
main()

23
gradle.properties Normal file
View File

@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

19
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,19 @@
[versions]
agp = "8.12.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
appcompat = "1.6.1"
material = "1.12.0"
coil = "2.5.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
coil = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Tue Aug 05 22:58:46 PDT 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored Executable file
View File

@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
library/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

37
library/build.gradle.kts Normal file
View File

@@ -0,0 +1,37 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "dev.oxmc.androiddeviceinfo"
compileSdk = 36
defaultConfig {
minSdk = 19
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
}

View File

21
library/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The library does not require any specific permissions, but if you want to auto update the db, you will need the INTERNET permisson. -->
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2017 Jared Rummler
* Copyright (C) 2025 oxmc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.oxmc.androiddevicenames
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.
*/
private val deviceNames: Map<Triple<List<String>, List<String>, List<String>>, String> = mapOf(
// 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",
// ZTE
Triple(listOf("Z6251"), listOf("ZTE"), listOf("Z6251")) to "ZMax 11",
// OnePlus
Triple(listOf("CPH2195"), listOf("Oppo"), listOf("")) to "OPPO A54 5G",
Triple(listOf("OP595DL1", "CPH2583"), listOf("Oppo"), listOf("")) to "OnePlus 12",
// Google
Triple(listOf("Pixel 6"), listOf("Google"), listOf("")) to "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",
// LG
Triple(listOf("LGLS675", "LS675"), listOf("LG"), listOf("LG M1", "")) to "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"
)
/**
* 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
}
}
}

View File

@@ -0,0 +1,185 @@
package dev.oxmc.androiddevicenames
import android.content.Context
import android.os.Build
object AndroidInfo {
object Info {
val brand: String = Build.BRAND
val model: String = Build.MODEL
val device: String = Build.DEVICE
val product: String = Build.PRODUCT
val manufacturer: String = Build.MANUFACTURER
val hardware: String = Build.HARDWARE
val board: String = Build.BOARD
val fingerprint: String = Build.FINGERPRINT
}
object Version {
val release: String = Build.VERSION.RELEASE
val incremental: String = Build.VERSION.INCREMENTAL
val sdkInt: Int = Build.VERSION.SDK_INT
val codename: String = Build.VERSION.CODENAME
}
enum class DeviceType {
EMULATOR, SMARTPHONE, TABLET, SMARTWATCH, SMART_TV,
AUTOMOTIVE, IOT, EREADER, GAMING, VR, FOLDABLE
}
fun getType(context: Context): DeviceType {
return when {
isEmulator -> DeviceType.EMULATOR
isSmartwatch() -> DeviceType.SMARTWATCH
isSmartTV() -> DeviceType.SMART_TV
isAutomotive() -> DeviceType.AUTOMOTIVE
isIoTDevice() -> DeviceType.IOT
isEReader() -> DeviceType.EREADER
isGamingDevice() -> DeviceType.GAMING
isVRDevice() -> DeviceType.VR
isFoldable() -> DeviceType.FOLDABLE
isTablet(context) -> DeviceType.TABLET
isSmartphone(context) -> DeviceType.SMARTPHONE
else -> DeviceType.SMARTPHONE
}
}
private fun deviceStartsWith(vararg prefixes: String): Boolean {
val value = Build.DEVICE.lowercase()
return prefixes.any { prefix -> value.startsWith(prefix.lowercase()) }
}
val isEmulator: Boolean by lazy {
listOf(
Build.FINGERPRINT to listOf("generic", "unknown"),
Build.MODEL to listOf("google_sdk", "Emulator", "Android SDK built for x86"),
Build.MANUFACTURER to listOf("Genymotion"),
Build.HARDWARE to listOf("goldfish", "ranchu")
).any { (value, keywords) ->
keywords.any { value.contains(it, ignoreCase = true) }
}
}
val isPhysicalDevice: Boolean get() = !isEmulator
private fun isSmartphone(context: Context): Boolean {
return !isEmulator && !isTablet(context) && !isSmartwatch() &&
!isSmartTV() && !isAutomotive() && !isIoTDevice() &&
!isEReader() && !isGamingDevice() && !isVRDevice() &&
!isFoldable()
}
private fun isTablet(context: Context): Boolean {
val metrics = context.resources.displayMetrics
val widthDp = metrics.widthPixels / metrics.density
val heightDp = metrics.heightPixels / metrics.density
val smallestWidth = minOf(widthDp, heightDp)
val isTabletBySize = smallestWidth >= 600
val model = Build.MODEL?.lowercase() ?: "unknown"
// Only check model for "tab" to identify tablets
val isTabletByModel = model.contains("tab")
return isTabletBySize || isTabletByModel
}
private fun isSmartwatch() = deviceStartsWith("wear", "smartwatch", "watch", "wrist")
private fun isSmartTV() = deviceStartsWith("tv", "television", "settopbox", "streaming", "media", "dongle")
private fun isAutomotive() = deviceStartsWith("car", "automotive", "infotainment", "dashboard", "headunit", "vehicle")
private fun isIoTDevice() = deviceStartsWith("iot", "smart", "connected", "home", "appliance", "device")
private fun isEReader() = deviceStartsWith("ereader", "ebook", "tablet", "kindle")
private fun isGamingDevice() = deviceStartsWith("gaming", "console", "handheld", "portable")
private fun isFoldable() = deviceStartsWith("fold", "flip", "clamshell", "booklet", "dual", "trifold")
private fun isQuest() = deviceStartsWith("quest", "oculus", "meta", "rift")
private fun isVRDevice(): Boolean {
return deviceStartsWith(
"vr", "virtual", "headset", "goggles", "glasses", "hmd", "helmet", "visor",
"mask", "smartglasses", "augmented", "mixed", "immersive", "spatial",
"3d", "360", "panoramic", "stereoscopic", "holographic", "projection",
"lightfield", "volumetric", "depth", "pointcloud", "sensor"
) || isQuest()
}
// VENDOR-SPECIFIC OBJECTS //
/* Most of the device-specific objects are empty stubs.
They will be populated with functions and properties specific to each device brand
as needed in the future. This allows for easy expansion and organization of device-specific logic.
*/
object Samsung {
// Samsung specific functions and properties
}
object Xiaomi {
// Xiaomi specific functions and properties
}
object Oppo {
// Oppo specific functions and properties
}
object Huawei {
// Huawei specific functions and properties
}
object OnePlus {
// OnePlus specific functions and properties
}
object Vivo {
// Vivo specific functions and properties
}
object LG {
// LG specific functions and properties
}
object Lenovo {
// Lenovo specific functions and properties
}
object Sony {
// Sony specific functions and properties
}
object Google {
// Google Pixel specific functions and properties
}
object Asus {
// Asus (ROG, ZenFone) specific functions and properties
}
object Realme {
// Realme specific functions and properties
}
object Motorola {
// Motorola (Lenovo) specific functions and properties
}
object Nokia {
// Nokia (HMD Global) specific functions and properties
}
object Honor {
// Honor (spun off from Huawei) specific functions and properties
}
object Infinix {
// Infinix (Transsion Holdings) specific functions and properties
}
object Tecno {
// Tecno (Transsion Holdings) specific functions and properties
}
object ZTE {
// ZTE and Nubia specific functions and properties
}
object Meizu {
// Meizu specific functions and properties
}
object Sharp {
// Sharp Aquos specific functions and properties
}
object Micromax {
// Micromax (India) specific functions and properties
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright (C) 2017 Jared Rummler
* Copyright (C) 2025 oxmc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.oxmc.androiddevicenames
import android.content.Context
import android.database.SQLException
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.text.TextUtils
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStreamReader
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.URL
import kotlin.Throws
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject
/**
* Database helper to access the list of all known Android devices with their market, code, and
* model name.
*/
class DeviceDatabase(context: Context, private val autoUpdateDB: Boolean = true) :
SQLiteOpenHelper(context, NAME, null, VERSION_CODE) {
private val file: File = context.getDatabasePath(NAME)
private val context: Context = context.applicationContext
init {
if (autoUpdateDB) {
checkForUpdates()
}
if (!file.exists()) {
create()
}
}
/**
* Query the market name given the codename and/or model of a device.
*
* @param codename the value of the system property "ro.product.device" ([Build.DEVICE]).
* @param model the value of the system property "ro.product.model" ([Build.MODEL]).
* @return The market name of the device if it is found in the database, otherwise null.
*/
fun query(codename: String?, model: String?): String? {
val database = readableDatabase
val columns = arrayOf(COLUMN_NAME)
val selection: String
val selectionArgs: Array<String>
if (codename != null && model != null) {
selection = "$COLUMN_CODENAME LIKE ? AND $COLUMN_MODEL LIKE ?"
selectionArgs = arrayOf(codename, model)
} else if (codename != null) {
selection = "$COLUMN_CODENAME LIKE ?"
selectionArgs = arrayOf(codename)
} else if (model != null) {
selection = "$COLUMN_MODEL LIKE ?"
selectionArgs = arrayOf(model)
} else {
return null
}
val cursor =
database.query(TABLE_DEVICES, columns, selection, selectionArgs, null, null, null)
var name: String? = null
if (cursor.moveToFirst()) {
name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME))
}
close(cursor)
close(database)
return name
}
/**
* Query the device info given the codename and/or model of a device.
*
* @param codename the value of the system property "ro.product.device" ([Build.DEVICE]).
* @param model the value of the system property "ro.product.model" ([Build.MODEL]).
* @return The [DeviceInfo] if it is found in the database, otherwise null.
*/
fun queryToDevice(codename: String?, model: String?): AndroidDeviceInfo.DeviceInfo? {
var codename = codename
var model = model
val database = readableDatabase
val columns = arrayOf(COLUMN_MANUFACTURER, COLUMN_NAME, COLUMN_CODENAME, COLUMN_MODEL)
val selection: String
val selectionArgs: Array<String?>
if (!TextUtils.isEmpty(codename) && !TextUtils.isEmpty(model)) {
selection = "$COLUMN_CODENAME LIKE ? AND $COLUMN_MODEL LIKE ?"
selectionArgs = arrayOf(codename, model)
} else if (!TextUtils.isEmpty(codename)) {
selection = "$COLUMN_CODENAME LIKE ?"
selectionArgs = arrayOf(codename)
} else if (!TextUtils.isEmpty(model)) {
selection = "$COLUMN_MODEL LIKE ?"
selectionArgs = arrayOf(model)
} else {
return null
}
val cursor =
database.query(TABLE_DEVICES, columns, selection, selectionArgs, null, null, null)
var deviceInfo: AndroidDeviceInfo.DeviceInfo? = null
if (cursor.moveToFirst()) {
val manufacturer = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MANUFACTURER))
val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME))
codename = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_CODENAME))
model = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MODEL))
deviceInfo = AndroidDeviceInfo.DeviceInfo(manufacturer, name, codename, model)
}
close(cursor)
close(database)
return deviceInfo
}
override fun onCreate(db: SQLiteDatabase) {
// no-op
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (newVersion > oldVersion) {
if (context.deleteDatabase(NAME) || file.delete() || !file.exists()) {
create()
}
}
}
@Throws(SQLException::class)
private fun create() {
try {
readableDatabase // Create an empty database that we will overwrite.
close() // Close the empty database
transferDatabaseAsset() // Copy the database from assets to the application's database dir
} catch (e: IOException) {
throw SQLException("Error creating $NAME database", e)
}
}
@Throws(IOException::class)
private fun transferDatabaseAsset() {
val input = context.assets.open(NAME)
val output: OutputStream = FileOutputStream(file)
val buffer = ByteArray(2048)
var length: Int
while ((input.read(buffer).also { length = it }) > 0) {
output.write(buffer, 0, length)
}
output.flush()
close(output)
close(input)
}
private fun checkForUpdates() {
CoroutineScope(Dispatchers.IO).launch {
try {
val url = URL(UPDATE_URL)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.connect()
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
val reader = InputStreamReader(inputStream)
val jsonResponse = reader.readText()
reader.close()
inputStream.close()
val jsonObject = JSONObject(jsonResponse)
val latestVersionString = jsonObject.getString("version")
val downloadUrl = jsonObject.getString("url")
if (isNewerVersion(latestVersionString, VERSION_NAME)) {
downloadAndUpdateDatabase(downloadUrl)
}
}
} catch (e: Exception) {
// Handle error during update check, e.g., log it
e.printStackTrace()
}
}
}
private suspend fun downloadAndUpdateDatabase(downloadUrl: String) = withContext(Dispatchers.IO) {
try {
val url = URL(downloadUrl)
val connection = url.openConnection() as HttpURLConnection
connection.connect()
context.deleteDatabase(NAME) // Delete old database
val outputStream = FileOutputStream(file)
connection.inputStream.copyTo(outputStream)
outputStream.close()
} catch (e: IOException) {
throw SQLException("Error downloading or updating $NAME database", e)
}
}
private fun isNewerVersion(version1: String, version2: String): Boolean {
val v1Parts = version1.split(".").map { it.toInt() }
val v2Parts = version2.split(".").map { it.toInt() }
val length = maxOf(v1Parts.size, v2Parts.size)
for (i in 0 until length) {
val part1 = if (i < v1Parts.size) v1Parts[i] else 0
val part2 = if (i < v2Parts.size) v2Parts[i] else 0
if (part1 > part2) {
return true
}
if (part1 < part2) {
return false
}
}
// Versions are equal
return false
}
private fun close(closeable: Closeable?) {
if (closeable != null) {
try {
closeable.close()
} catch (ignored: IOException) {
}
}
}
companion object {
private const val TABLE_DEVICES = "devices"
private const val COLUMN_MANUFACTURER = "manufacturer"
private const val COLUMN_NAME = "name"
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 UPDATE_URL = "https://cdn.oxmc.me/android-devicenames/latest.json"
}
}

View File

@@ -0,0 +1,191 @@
/*
* Copyright (C) 2017 Jared Rummler
* Copyright (C) 2025 oxmc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.oxmc.androiddevicenames
import android.content.Context
import android.os.Build
import android.provider.Settings
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.BufferedReader
import java.net.HttpURLConnection
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";
/**
* Get device information including name, manufacturer, model, codename, and image URL.
*
* @param context The application context.
* @return An array containing device name, manufacturer, model, codename, and image URL.
*/
suspend fun getDeviceInfo(context: Context): Array<String?> {
// Get basic device information
val deviceInfo = AndroidDeviceInfo.getCurrentDeviceInfo(context)
// Set all the device information
val deviceName = deviceInfo.name ?: "Unknown"
val manufacturer = deviceInfo.manufacturer ?: "Unknown"
val model = deviceInfo.model ?: "Unknown"
val codename = deviceInfo.codename ?: "Unknown"
// Get the device image if it exists
val deviceImage = getDeviceImage(context, codename, model, manufacturer)
// Return the device information
return arrayOf(deviceName, manufacturer, model, codename, deviceImage)
}
/**
* Retrieves the device name.
*
* For Android N (API level 24) and above, it attempts to get the name from `Settings.Global.DEVICE_NAME`.
* For Android Q (API level 29) and above, it attempts to get the name from `Settings.Secure.DEVICE_NAME`.
* If these fail or for older versions, it falls back to the manufacturer and model.
*
* @param context The application context.
* @return The device name.
*/
fun getDeviceName(context: Context): String {
var deviceName: String? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
deviceName = Settings.Global.getString(context.contentResolver, Settings.Global.DEVICE_NAME)
}
if (deviceName.isNullOrEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Settings.Secure.DEVICE_NAME was added in API level 29 (Android Q)
// but let's check for it specifically in case of custom ROMs or backports.
// The constant "device_name" is what is used internally.
deviceName = Settings.Secure.getString(context.contentResolver, "device_name")
}
return if (!deviceName.isNullOrEmpty()) {
deviceName
} else {
// Fallback to manufacturer and model
"${Build.MANUFACTURER} ${Build.MODEL}"
}
}
/**
* Fetches the device image URL from the API based on the device's codename, model, and manufacturer.
*
* @param context The application context.
* @param codename The device codename.
* @param model The device model.
* @param manufacturer The device manufacturer.
* @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 baseUrl = "${api_url_base}/${api_deviceSearch}/${api_version}"
val queryParams = mutableListOf<String>()
// 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.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
} else {
println("HTTP Error: ${connection.responseCode} ${connection.responseMessage} for URL: $fullUrl")
}
} catch (e: Exception) {
e.printStackTrace()
} finally {
connection?.disconnect()
}
}
return deviceImage
}
/**
* Reports device information to the server.
*
* @param context The application context.
* @param deviceName The name of the device.
* @param manufacturer The manufacturer of the device.
* @param model The model of the device.
* @param codename The codename of the device.
* @return True if the report was successful, false otherwise.
*/
@Suppress("unused")
suspend fun reportDeviceInfo(context: Context, deviceName: String, manufacturer: String, model: String, codename: String): Boolean {
return withContext(Dispatchers.IO) {
val jsonData = JSONObject().apply {
put("deviceName", deviceName)
put("manufacturer", manufacturer)
put("model", model)
put("codename", codename)
}
sendDeviceInfoToServer(context, jsonData.toString())
}
}
/**
* Sends device information to the server.
*
* @param context The application context.
* @param jsonData The JSON data containing device information.
* @return True if the request was successful, false otherwise.
*/
private suspend fun sendDeviceInfoToServer(context: Context, jsonData: String): Boolean {
val urlString = "${api_url_base}/${api_reportDevice}"
return withContext(Dispatchers.IO) {
try {
val connection = URL(urlString).openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json")
connection.doOutput = true
connection.outputStream.writer().use { it.write(jsonData) }
connection.responseCode == HttpURLConnection.HTTP_OK
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) 2017 Jared Rummler
* Copyright (C) 2025 oxmc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.oxmc.androiddevicenames
import android.content.Context
import android.os.Build
import android.text.TextUtils
import androidx.annotation.WorkerThread
import androidx.core.content.edit
import org.json.JSONException
import org.json.JSONObject
import java.util.concurrent.TimeUnit
object AndroidDeviceInfo {
private const val SHARED_PREF_NAME = "device_names"
private val CACHE_DURATION = TimeUnit.DAYS.toMillis(30)
//fun getDeviceNameFromCodename(codename: String?, fallback: String?): String? {
// return getDeviceName(codename, codename, fallback)
//}
//fun getDeviceName(deviceInfo: DeviceInfo, fallback: String?): String? {
// return deviceInfo.marketName ?: fallback
//}
@WorkerThread
fun getCurrentDeviceInfo(context: Context?): DeviceInfo {
val codename = Build.PRODUCT
val model = Build.MODEL
val cachedInfo = getDeviceInfoFromCache(context, codename, model)
if (cachedInfo != null && isCacheValid(cachedInfo)) {
return cachedInfo
}
val info = getDeviceInfo(context, codename, model)
storeDeviceInfoInCache(context, info)
return info
}
@WorkerThread
private fun getDeviceInfo(context: Context?, codename: String, model: String): DeviceInfo {
val manufacturer = Build.MANUFACTURER
try {
DeviceDatabase(context!!).use { database ->
val info: DeviceInfo? = database.queryToDevice(codename, model)
if (info != null) {
return info
}
}
} catch (e: Exception) {
e.printStackTrace()
}
// Attempt to get the alternate name of the device
val altName = AltDeviceNames.getAlternateNameOrDefault(capitalize(model), capitalize(manufacturer), codename, codename)
val finalDeviceName: String = if (!TextUtils.isEmpty(altName)) {
altName
} else {
model
}
//Log.i("DeviceInfo", "Device name: $finalDeviceName, codename: $codename, model: $model, manufacturer: $manufacturer")
return DeviceInfo(manufacturer, finalDeviceName, codename, model)
}
private fun getDeviceInfoFromCache(
context: Context?,
codename: String,
model: String
): DeviceInfo? {
val prefs = context!!.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val key = String.format("%s:%s", codename, model)
val savedJson = prefs.getString(key, null)
if (savedJson != null) {
try {
val jsonObject = JSONObject(savedJson)
val timestamp = jsonObject.optLong("timestamp", 0)
return DeviceInfo(jsonObject, timestamp)
} catch (e: JSONException) {
e.printStackTrace()
prefs.edit() { remove(key) }
}
}
return null
}
private fun isCacheValid(info: DeviceInfo): Boolean {
val now = System.currentTimeMillis()
return (now - info.timestamp) < CACHE_DURATION
}
private fun storeDeviceInfoInCache(context: Context?, info: DeviceInfo) {
val prefs = context!!.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
val key = String.format("%s:%s", info.codename, info.model)
try {
val json = JSONObject()
json.put("manufacturer", info.manufacturer)
json.put("codename", info.codename)
json.put("model", info.model)
json.put("market_name", info.marketName)
json.put("timestamp", System.currentTimeMillis())
prefs.edit() { putString(key, json.toString()) }
} catch (e: JSONException) {
e.printStackTrace()
}
}
private fun capitalize(str: String): String {
if (TextUtils.isEmpty(str)) {
return str
}
return str.uppercase()
}
class DeviceInfo @JvmOverloads constructor(
val manufacturer: String?,
val marketName: String?,
val codename: String,
val model: String,
val timestamp: Long = 0
) {
constructor(jsonObject: JSONObject, timestamp: Long) : this(
jsonObject.getString("manufacturer"),
jsonObject.getString("market_name"),
jsonObject.getString("codename"),
jsonObject.getString("model"),
timestamp
)
val name: String
get() {
// Return the marketName if it's not null or empty.
// Otherwise, fall back to the capitalized model name.
return if (!marketName.isNullOrEmpty()) {
marketName
} else {
capitalize(model)
}
}
}
}

24
settings.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "AndroidDeviceNames"
include(":demo")
include(":library")