package com.bugsnag.android; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.os.Build; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.DisplayMetrics; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Locale; /** * Information about the current Android device which doesn't change over time, * including screen and locale information. * <p/> * App information in this class is cached during construction for faster * subsequent lookups and to reduce GC overhead. */ class DeviceData implements JsonStream.Streamable { protected final Float screenDensity; protected final Integer dpi; protected final String screenResolution; protected final Long totalMemory; protected final Boolean rooted; protected final String locale; protected final String id; protected final String[] cpuAbi; DeviceData(@NonNull Context appContext) { screenDensity = getScreenDensity(appContext); dpi = getScreenDensityDpi(appContext); screenResolution = getScreenResolution(appContext); totalMemory = getTotalMemory(); rooted = isRooted(); locale = getLocale(); id = getAndroidId(appContext); cpuAbi = getCpuAbi(); } public void toStream(@NonNull JsonStream writer) throws IOException { writer.beginObject(); writer.name("osName").value("android"); writer.name("manufacturer").value(android.os.Build.MANUFACTURER); writer.name("brand").value(android.os.Build.BRAND); writer.name("model").value(android.os.Build.MODEL); writer.name("id").value(id); writer.name("apiLevel").value(android.os.Build.VERSION.SDK_INT); writer.name("osVersion").value(android.os.Build.VERSION.RELEASE); writer.name("osBuild").value(android.os.Build.DISPLAY); writer.name("locale").value(locale); writer.name("totalMemory").value(totalMemory); writer.name("jailbroken").value(rooted); writer.name("screenDensity").value(screenDensity); writer.name("dpi").value(dpi); writer.name("screenResolution").value(screenResolution); writer.name("cpuAbi").beginArray(); for (String s : cpuAbi) { writer.value(s); } writer.endArray(); writer.endObject(); } public String getUserId() { return id; } /** * The screen density scaling factor of the current Android device */ @Nullable private static Float getScreenDensity(Context appContext) { Resources resources = appContext.getResources(); if (resources == null) return null; return resources.getDisplayMetrics().density; } /** * The screen density of the current Android device in dpi, eg. 320 */ @Nullable private static Integer getScreenDensityDpi(Context appContext) { Resources resources = appContext.getResources(); if (resources == null) return null; return resources.getDisplayMetrics().densityDpi; } /** * The screen resolution of the current Android device in px, eg. 1920x1080 */ @Nullable private static String getScreenResolution(Context appContext) { Resources resources = appContext.getResources(); if (resources == null) return null; DisplayMetrics metrics = resources.getDisplayMetrics(); return String.format(Locale.US, "%dx%d", Math.max(metrics.widthPixels, metrics.heightPixels), Math.min(metrics.widthPixels, metrics.heightPixels)); } /** * Get the total memory available on the current Android device, in bytes */ @NonNull private static Long getTotalMemory() { if (Runtime.getRuntime().maxMemory() != Long.MAX_VALUE) { return Runtime.getRuntime().maxMemory(); } else { return Runtime.getRuntime().totalMemory(); } } private static final String[] ROOT_INDICATORS = new String[]{ // Common binaries "/system/xbin/su", "/system/bin/su", // < Android 5.0 "/system/app/Superuser.apk", "/system/app/SuperSU.apk", // >= Android 5.0 "/system/app/Superuser", "/system/app/SuperSU", // Fallback "/system/xbin/daemonsu", // Systemless root "/su/bin" }; /** * Check if the current Android device is rooted */ @Nullable private static Boolean isRooted() { if (android.os.Build.TAGS != null && android.os.Build.TAGS.contains("test-keys")) return true; try { for (String candidate : ROOT_INDICATORS) { if (new File(candidate).exists()) return true; } } catch (Exception ignore) { return null; } return false; } /** * Get the locale of the current Android device, eg en_US */ @NonNull private static String getLocale() { return Locale.getDefault().toString(); } /** * Get the unique device id for the current Android device */ @NonNull private static String getAndroidId(Context appContext) { ContentResolver cr = appContext.getContentResolver(); return Settings.Secure.getString(cr, Settings.Secure.ANDROID_ID); } /** * Gets information about the CPU / API */ @NonNull private static String[] getCpuAbi() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return SupportedAbiWrapper.getSupportedAbis(); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { return Abi2Wrapper.getAbi1andAbi2(); } return new String[]{Build.CPU_ABI}; } /** * Wrapper class to allow the test framework to use the correct version of the CPU / ABI */ private static class SupportedAbiWrapper { @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static String[] getSupportedAbis() { return Build.SUPPORTED_ABIS; } } /** * Wrapper class to allow the test framework to use the correct version of the CPU / ABI */ private static class Abi2Wrapper { @TargetApi(Build.VERSION_CODES.FROYO) public static String[] getAbi1andAbi2() { return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; } } }