package com.getsentry.raven.android.event.helper; import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.BatteryManager; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.provider.Settings; import android.util.DisplayMetrics; import android.util.Log; import com.getsentry.raven.android.Util; import com.getsentry.raven.environment.RavenEnvironment; import com.getsentry.raven.event.EventBuilder; import com.getsentry.raven.event.helper.EventBuilderHelper; import com.getsentry.raven.event.interfaces.UserInterface; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; import static android.content.Context.ACTIVITY_SERVICE; /** * EventBuilderHelper that makes use of Android Context to populate some Event fields. */ public class AndroidEventBuilderHelper implements EventBuilderHelper { /** * Logger tag. */ public static final String TAG = AndroidEventBuilderHelper.class.getName(); private static final Boolean IS_EMULATOR = isEmulator(); private static final String KERNEL_VERSION = getKernelVersion(); private Context ctx; /** * Construct given the provided Android {@link Context}. * * @param ctx Android application context. */ public AndroidEventBuilderHelper(Context ctx) { this.ctx = ctx; } @Override public void helpBuildingEvent(EventBuilder eventBuilder) { eventBuilder.withSdkName(RavenEnvironment.SDK_NAME + ":android"); PackageInfo packageInfo = getPackageInfo(ctx); if (packageInfo != null) { eventBuilder.withRelease(packageInfo.versionName); } String androidId = Settings.Secure.getString(ctx.getContentResolver(), Settings.Secure.ANDROID_ID); if (androidId != null && !androidId.trim().equals("")) { UserInterface userInterface = new UserInterface("android:" + androidId, null, null, null); // set user interface but *don't* replace if it's already there eventBuilder.withSentryInterface(userInterface, false); } eventBuilder.withContexts(getContexts()); } private Map<String, Map<String, Object>> getContexts() { Map<String, Map<String, Object>> contexts = new HashMap<>(); Map<String, Object> deviceMap = new HashMap<>(); Map<String, Object> osMap = new HashMap<>(); Map<String, Object> appMap = new HashMap<>(); contexts.put("os", osMap); contexts.put("device", deviceMap); contexts.put("app", appMap); // Device deviceMap.put("manufacturer", Build.MANUFACTURER); deviceMap.put("brand", Build.BRAND); deviceMap.put("model", Build.MODEL); deviceMap.put("family", getFamily()); deviceMap.put("model_id", Build.ID); deviceMap.put("battery_level", getBatteryLevel(ctx)); deviceMap.put("orientation", getOrientation(ctx)); deviceMap.put("simulator", IS_EMULATOR); deviceMap.put("arch", Build.CPU_ABI); deviceMap.put("storage_size", getTotalInternalStorage()); deviceMap.put("free_storage", getUnusedInternalStorage()); deviceMap.put("external_storage_size", getTotalExternalStorage()); deviceMap.put("external_free_storage", getUnusedExternalStorage()); deviceMap.put("charging", isCharging(ctx)); deviceMap.put("online", Util.isConnected(ctx)); DisplayMetrics displayMetrics = getDisplayMetrics(ctx); if (displayMetrics != null) { int largestSide = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); int smallestSide = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels); String resolution = Integer.toString(largestSide) + "x" + Integer.toString(smallestSide); deviceMap.put("screen_resolution", resolution); deviceMap.put("screen_density", displayMetrics.density); deviceMap.put("screen_dpi", displayMetrics.densityDpi); } ActivityManager.MemoryInfo memInfo = getMemInfo(ctx); if (memInfo != null) { deviceMap.put("free_memory", memInfo.availMem); deviceMap.put("memory_size", memInfo.totalMem); deviceMap.put("low_memory", memInfo.lowMemory); } // Operating System osMap.put("name", "Android"); osMap.put("version", Build.VERSION.RELEASE); osMap.put("build", Build.DISPLAY); osMap.put("kernel_version", KERNEL_VERSION); osMap.put("rooted", isRooted()); // App PackageInfo packageInfo = getPackageInfo(ctx); if (packageInfo != null) { appMap.put("app_version", packageInfo.versionName); appMap.put("app_build", packageInfo.versionCode); appMap.put("app_identifier", packageInfo.packageName); } appMap.put("app_name", getApplicationName(ctx)); appMap.put("app_start_time", stringifyDate(new Date())); return contexts; } /** * Return the Application's PackageInfo if possible, or null. * * @param ctx Android application context * @return the Application's PackageInfo if possible, or null */ private static PackageInfo getPackageInfo(Context ctx) { try { return ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Error getting package info.", e); return null; } } /** * Fake the device family by using the first word in the Build.MODEL. Works * well in most cases... "Nexus 6P" -> "Nexus", "Galaxy S7" -> "Galaxy". * * @return family name of the device, as best we can tell */ private static String getFamily() { try { return Build.MODEL.split(" ")[0]; } catch (Exception e) { Log.e(TAG, "Error getting device family.", e); return null; } } /** * Check whether the application is running in an emulator. http://stackoverflow.com/a/21505193 * * @return true if the application is running in an emulator, false otherwise */ private static Boolean isEmulator() { try { return Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") || Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || "google_sdk".equals(Build.PRODUCT); } catch (Exception e) { Log.e(TAG, "Error checking whether application is running in an emulator.", e); return null; } } /** * Get MemoryInfo object representing the memory state of the application. * * @param ctx Android application context * @return MemoryInfo object representing the memory state of the application */ private static ActivityManager.MemoryInfo getMemInfo(Context ctx) { try { ActivityManager actManager = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE); ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); actManager.getMemoryInfo(memInfo); return memInfo; } catch (Exception e) { Log.e(TAG, "Error getting MemoryInfo.", e); return null; } } /** * Get the device's current screen orientation. * * @param ctx Android application context * @return the device's current screen orientation, or null if unknown */ private static String getOrientation(Context ctx) { try { String o; switch (ctx.getResources().getConfiguration().orientation) { case android.content.res.Configuration.ORIENTATION_LANDSCAPE: o = "landscape"; break; case android.content.res.Configuration.ORIENTATION_PORTRAIT: o = "portrait"; break; default: o = null; break; } return o; } catch (Exception e) { Log.e(TAG, "Error getting device orientation.", e); return null; } } /** * Get the device's current battery level (as a percentage of total). * * @param ctx Android application context * @return the device's current battery level (as a percentage of total), or null if unknown */ private static Float getBatteryLevel(Context ctx) { try { Intent intent = ctx.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (intent == null) { return null; } int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1); int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1); if (level == -1 || scale == -1) { return null; } // CHECKSTYLE.OFF: MagicNumber float percentMultiplier = 100.0f; // CHECKSTYLE.ON: MagicNumber return ((float) level / (float) scale) * percentMultiplier; } catch (Exception e) { Log.e(TAG, "Error getting device battery level.", e); return null; } } /** * Checks whether or not the device is currently plugged in and charging, or null if unknown. * * @param ctx Android application context * @return whether or not the device is currently plugged in and charging, or null if unknown */ private static Boolean isCharging(Context ctx) { try { Intent intent = ctx.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); if (intent == null) { return null; } int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); return plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB; } catch (Exception e) { Log.e(TAG, "Error getting device charging state.", e); return null; } } /** * Get the device's current kernel version, as a string (from uname -a). * * @return the device's current kernel version, as a string (from uname -a) */ private static String getKernelVersion() { String errorMsg = "Exception while attempting to read kernel information"; BufferedReader br = null; try { Process p = Runtime.getRuntime().exec("uname -a"); if (p.waitFor() == 0) { br = new BufferedReader(new InputStreamReader(p.getInputStream())); return br.readLine(); } } catch (Exception e) { Log.e(TAG, errorMsg, e); } finally { if (br != null) { try { br.close(); } catch (IOException ioe) { Log.e(TAG, errorMsg, ioe); } } } return null; } /** * Attempt to discover if this device is currently rooted. From: * https://stackoverflow.com/questions/1101380/determine-if-running-on-a-rooted-device * * @return true if heuristics show the device is probably rooted, otherwise false */ private static Boolean isRooted() { if (android.os.Build.TAGS != null && android.os.Build.TAGS.contains("test-keys")) { return true; } String[] probableRootPaths = { "/data/local/bin/su", "/data/local/su", "/data/local/xbin/su", "/sbin/su", "/su/bin", "/su/bin/su", "/system/app/SuperSU", "/system/app/SuperSU.apk", "/system/app/Superuser", "/system/app/Superuser.apk", "/system/bin/failsafe/su", "/system/bin/su", "/system/sd/xbin/su", "/system/xbin/daemonsu", "/system/xbin/su" }; for (String probableRootPath : probableRootPaths) { try { if (new File(probableRootPath).exists()) { return true; } } catch (Exception e) { Log.e(TAG, "Exception while attempting to detect whether the device is rooted", e); } } return false; } private static boolean isExternalStorageMounted() { return Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED) && !Environment.isExternalStorageEmulated(); } /** * Get the unused amount of internal storage, in bytes. * * @return the unused amount of internal storage, in bytes */ private static Long getUnusedInternalStorage() { try { File path = Environment.getDataDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSize(); long availableBlocks = stat.getAvailableBlocks(); return availableBlocks * blockSize; } catch (Exception e) { Log.e(TAG, "Error getting unused internal storage amount.", e); return null; } } /** * Get the total amount of internal storage, in bytes. * * @return the total amount of internal storage, in bytes */ private static Long getTotalInternalStorage() { try { File path = Environment.getDataDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSize(); long totalBlocks = stat.getBlockCount(); return totalBlocks * blockSize; } catch (Exception e) { Log.e(TAG, "Error getting total internal storage amount.", e); return null; } } /** * Get the unused amount of external storage, in bytes, or null if no external storage * is mounted. * * @return the unused amount of external storage, in bytes, or null if no external storage * is mounted */ private static Long getUnusedExternalStorage() { try { if (isExternalStorageMounted()) { File path = Environment.getExternalStorageDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSize(); long availableBlocks = stat.getAvailableBlocks(); return availableBlocks * blockSize; } } catch (Exception e) { Log.e(TAG, "Error getting unused external storage amount.", e); } return null; } /** * Get the total amount of external storage, in bytes, or null if no external storage * is mounted. * * @return the total amount of external storage, in bytes, or null if no external storage * is mounted */ private static Long getTotalExternalStorage() { try { if (isExternalStorageMounted()) { File path = Environment.getExternalStorageDirectory(); StatFs stat = new StatFs(path.getPath()); long blockSize = stat.getBlockSize(); long totalBlocks = stat.getBlockCount(); return totalBlocks * blockSize; } } catch (Exception e) { Log.e(TAG, "Error getting total external storage amount.", e); } return null; } /** * Get the DisplayMetrics object for the current application. * * @param ctx Android application context * @return the DisplayMetrics object for the current application */ private static DisplayMetrics getDisplayMetrics(Context ctx) { try { return ctx.getResources().getDisplayMetrics(); } catch (Exception e) { Log.e(TAG, "Error getting DisplayMetrics.", e); return null; } } /** * Formats the given Date object into an ISO8601 String. Note that SimpleDateFormat isn't * thread safe, and so we build one every time. * * @param date Date to format as ISO8601 * @return String representing the provided Date in ISO8601 format */ private static String stringifyDate(Date date) { return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(date); } /** * Get the human-facing Application name. * * @param ctx Android application context * @return Application name */ private static String getApplicationName(Context ctx) { try { ApplicationInfo applicationInfo = ctx.getApplicationInfo(); int stringId = applicationInfo.labelRes; if (stringId == 0) { if (applicationInfo.nonLocalizedLabel != null) { return applicationInfo.nonLocalizedLabel.toString(); } } else { return ctx.getString(stringId); } } catch (Exception e) { Log.e(TAG, "Error getting application name.", e); } return null; } }