commit 354f1a6f96be0acc9a7344955cd0d756457d9c52 Author: oxmc7769 Date: Wed Aug 6 17:26:20 2025 -0700 first upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc493a1 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..ecf7a23 --- /dev/null +++ b/build.gradle.kts @@ -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 +} \ No newline at end of file diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts new file mode 100644 index 0000000..f35d50f --- /dev/null +++ b/demo/build.gradle.kts @@ -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")) +} \ No newline at end of file diff --git a/demo/proguard-rules.pro b/demo/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/demo/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3245763 --- /dev/null +++ b/demo/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/java/dev/oxmc/androiddeviceinfo_demo/MainActivity.kt b/demo/src/main/java/dev/oxmc/androiddeviceinfo_demo/MainActivity.kt new file mode 100644 index 0000000..e25784d --- /dev/null +++ b/demo/src/main/java/dev/oxmc/androiddeviceinfo_demo/MainActivity.kt @@ -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}" + } + } + } +} \ No newline at end of file diff --git a/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/ic_launcher_background.xml b/demo/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/demo/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher.webp b/demo/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/demo/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher.webp b/demo/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/demo/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp b/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/demo/src/main/res/values-night/themes.xml b/demo/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..7c79dfa --- /dev/null +++ b/demo/src/main/res/values-night/themes.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/demo/src/main/res/values/colors.xml b/demo/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/demo/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000..06ba154 --- /dev/null +++ b/demo/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + AndroidDeviceNames + \ No newline at end of file diff --git a/demo/src/main/res/values/themes.xml b/demo/src/main/res/values/themes.xml new file mode 100644 index 0000000..b632bbf --- /dev/null +++ b/demo/src/main/res/values/themes.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/demo/src/main/res/xml/backup_rules.xml b/demo/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..4df9255 --- /dev/null +++ b/demo/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/demo/src/main/res/xml/data_extraction_rules.xml b/demo/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/demo/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/gen-db.py b/gen-db.py new file mode 100644 index 0000000..5c1698a --- /dev/null +++ b/gen-db.py @@ -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() + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..20e2a01 --- /dev/null +++ b/gradle.properties @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..0fbb5df --- /dev/null +++ b/gradle/libs.versions.toml @@ -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" } + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d51a760 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/library/.gitignore b/library/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/library/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 0000000..204564b --- /dev/null +++ b/library/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/library/consumer-rules.pro b/library/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/library/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2eb570e --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/library/src/main/java/dev/oxmc/androiddevicenames/AltDeviceNames.kt b/library/src/main/java/dev/oxmc/androiddevicenames/AltDeviceNames.kt new file mode 100644 index 0000000..ffa1942 --- /dev/null +++ b/library/src/main/java/dev/oxmc/androiddevicenames/AltDeviceNames.kt @@ -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, List, List>, 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 + } + } +} \ No newline at end of file diff --git a/library/src/main/java/dev/oxmc/androiddevicenames/AndroidInfo.kt b/library/src/main/java/dev/oxmc/androiddevicenames/AndroidInfo.kt new file mode 100644 index 0000000..1bdfba3 --- /dev/null +++ b/library/src/main/java/dev/oxmc/androiddevicenames/AndroidInfo.kt @@ -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 + } +} \ No newline at end of file diff --git a/library/src/main/java/dev/oxmc/androiddevicenames/DBHelper.kt b/library/src/main/java/dev/oxmc/androiddevicenames/DBHelper.kt new file mode 100644 index 0000000..189907d --- /dev/null +++ b/library/src/main/java/dev/oxmc/androiddevicenames/DBHelper.kt @@ -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 + 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 + + 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" + } +} \ No newline at end of file diff --git a/library/src/main/java/dev/oxmc/androiddevicenames/DeviceInfo.kt b/library/src/main/java/dev/oxmc/androiddevicenames/DeviceInfo.kt new file mode 100755 index 0000000..75a8aa5 --- /dev/null +++ b/library/src/main/java/dev/oxmc/androiddevicenames/DeviceInfo.kt @@ -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 { + // 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() + + // 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 + } + } + } +} \ No newline at end of file diff --git a/library/src/main/java/dev/oxmc/androiddevicenames/DeviceName.kt b/library/src/main/java/dev/oxmc/androiddevicenames/DeviceName.kt new file mode 100644 index 0000000..e5ce2ab --- /dev/null +++ b/library/src/main/java/dev/oxmc/androiddevicenames/DeviceName.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..302f565 --- /dev/null +++ b/settings.gradle.kts @@ -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")