diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java index d58176d631..b1d769eee6 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -11,6 +11,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -351,4 +352,29 @@ public void testDeviceDataSummary() { assertNotEquals(client.getDeviceDataSummary(), deviceData); } + @Test + public void testPopulateDeviceMetadata() { + Client client = generateClient(); + MetaData metaData = new MetaData(); + Map app = metaData.getTab("device"); + assertEquals(0, app.size()); + + client.populateDeviceMetaData(metaData); + assertEquals(14, app.size()); + + assertNotNull(app.get("batteryLevel")); + assertNotNull(app.get("charging")); + assertNotNull(app.get("locationStatus")); + assertNotNull(app.get("networkAccess")); + assertNotNull(app.get("time")); + assertNotNull(app.get("brand")); + assertNotNull(app.get("apiLevel")); + assertNotNull(app.get("osBuild")); + assertNotNull(app.get("locale")); + assertNotNull(app.get("screenDensity")); + assertNotNull(app.get("dpi")); + assertNotNull(app.get("emulator")); + assertNotNull(app.get("screenResolution")); + assertNotNull(app.get("cpuAbi")); + } } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java index 1696d4a8f2..3f5e4a9611 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java @@ -1,5 +1,6 @@ package com.bugsnag.android; +import static com.bugsnag.android.BugsnagTestUtils.generateClient; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -11,6 +12,7 @@ import org.json.JSONException; import org.json.JSONObject; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -25,7 +27,13 @@ public class DeviceDataSummaryTest { @Before public void setUp() throws Exception { - deviceData = new DeviceDataSummary(); + DeviceDataCollector deviceDataCollector = new DeviceDataCollector(generateClient()); + deviceData = deviceDataCollector.generateDeviceDataSummary(); + } + + @After + public void tearDown() throws Exception { + Async.cancelTasks(); } @Test diff --git a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java index 0942252efd..03004ec2e6 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java @@ -1,5 +1,6 @@ package com.bugsnag.android; +import static com.bugsnag.android.BugsnagTestUtils.generateClient; import static com.bugsnag.android.BugsnagTestUtils.getSharedPrefs; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; @@ -13,6 +14,7 @@ import org.json.JSONException; import org.json.JSONObject; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -28,8 +30,13 @@ public class DeviceDataTest { @Before public void setUp() throws Exception { - SharedPreferences sharedPref = getSharedPrefs(InstrumentationRegistry.getContext()); - deviceData = new DeviceData(InstrumentationRegistry.getContext(), sharedPref); + DeviceDataCollector deviceDataCollector = new DeviceDataCollector(generateClient()); + deviceData = deviceDataCollector.generateDeviceData(); + } + + @After + public void tearDown() throws Exception { + Async.cancelTasks(); } @Test diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java index 9378840f2f..ab6aad3414 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java @@ -19,6 +19,7 @@ import org.json.JSONException; import org.json.JSONObject; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -45,6 +46,11 @@ public void setUp() throws Exception { error = new Error.Builder(config, exception, null).build(); } + @After + public void tearDown() throws Exception { + Async.cancelTasks(); + } + @Test public void testShouldIgnoreClass() { config.setIgnoreClasses(new String[]{"java.io.IOException"}); @@ -369,8 +375,8 @@ public void testErrorMetaData() { @Test public void testSetDeviceId() throws Throwable { - Context context = InstrumentationRegistry.getContext(); - DeviceData deviceData = new DeviceData(context, BugsnagTestUtils.getSharedPrefs(context)); + DeviceDataCollector deviceDataCollector = new DeviceDataCollector(generateClient()); + DeviceData deviceData = deviceDataCollector.generateDeviceData(); error.setDeviceData(deviceData); assertEquals(deviceData, error.getDeviceData()); diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index c99f8f0c59..e5d29d25bd 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -70,6 +70,8 @@ public class Client extends Observable implements Observer { @NonNull protected final DeviceData deviceData; + DeviceDataCollector deviceDataCollector; + @NonNull final Breadcrumbs breadcrumbs; @@ -83,7 +85,7 @@ public class Client extends Observable implements Observer { private final EventReceiver eventReceiver; final SessionTracker sessionTracker; - private SharedPreferences sharedPref; + SharedPreferences sharedPrefs; AppDataCollector appDataCollector; /** @@ -144,13 +146,13 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat eventReceiver = new EventReceiver(this); // Set up and collect constant app and device diagnostics - sharedPref = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); - + sharedPrefs = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); appDataCollector = new AppDataCollector(this); appData = appDataCollector.generateAppData(); - deviceData = new DeviceData(appContext, sharedPref); + deviceDataCollector = new DeviceDataCollector(this); + deviceData = deviceDataCollector.generateDeviceData(); // Set up breadcrumbs breadcrumbs = new Breadcrumbs(); @@ -160,9 +162,9 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat if (config.getPersistUserBetweenSessions()) { // Check to see if a user was stored in the SharedPreferences - user.setId(sharedPref.getString(USER_ID_KEY, deviceData.getId())); - user.setName(sharedPref.getString(USER_NAME_KEY, null)); - user.setEmail(sharedPref.getString(USER_EMAIL_KEY, null)); + user.setId(sharedPrefs.getString(USER_ID_KEY, deviceData.getId())); + user.setName(sharedPrefs.getString(USER_NAME_KEY, null)); + user.setEmail(sharedPrefs.getString(USER_EMAIL_KEY, null)); } else { user.setId(deviceData.getId()); } @@ -582,13 +584,18 @@ public void populateAppMetaData(@NonNull MetaData metaData) { @NonNull @InternalApi public DeviceData getDeviceData() { - return new DeviceData(appContext, sharedPref); + return deviceDataCollector.generateDeviceData(); } @NonNull @InternalApi public DeviceDataSummary getDeviceDataSummary() { - return new DeviceDataSummary(); + return deviceDataCollector.generateDeviceDataSummary(); + } + + @InternalApi + public void populateDeviceMetaData(@NonNull MetaData metaData) { + deviceDataCollector.populateDeviceMetaData(metaData); } /** @@ -942,12 +949,14 @@ void notify(@NonNull Error error, } // Capture the state of the app and device and attach diagnostics to the error + DeviceData errorDeviceData = deviceDataCollector.generateDeviceData(); + error.setDeviceData(errorDeviceData); + deviceDataCollector.populateDeviceMetaData(error.getMetaData()); + error.setAppData(errorAppData); - error.setDeviceData(deviceData); // add additional info that belongs in metadata appDataCollector.populateAppMetaData(error.getMetaData()); - deviceData.addDeviceMetaData(error.getMetaData()); // Attach breadcrumbs to the error error.setBreadcrumbs(breadcrumbs); diff --git a/sdk/src/main/java/com/bugsnag/android/DeviceData.java b/sdk/src/main/java/com/bugsnag/android/DeviceData.java index bee5eec614..db4ea75d07 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeviceData.java +++ b/sdk/src/main/java/com/bugsnag/android/DeviceData.java @@ -1,39 +1,16 @@ package com.bugsnag.android; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.BatteryManager; -import android.os.Build; -import android.os.Environment; -import android.os.StatFs; -import android.provider.Settings; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; -import android.util.DisplayMetrics; import java.io.IOException; -import java.util.Date; -import java.util.Locale; -import java.util.UUID; /** * Information about the current Android device which doesn't change over time, * including screen and locale information. - *

- * App information in this class is cached during construction for faster - * subsequent lookups and to reduce GC overhead. */ public class DeviceData extends DeviceDataSummary { - private static final String INSTALL_ID_KEY = "install.iud"; - private long freeMemory; private long totalMemory; @@ -46,36 +23,6 @@ public class DeviceData extends DeviceDataSummary { @Nullable private String orientation; - @Nullable - final Float screenDensity; - - @Nullable - final Integer dpi; - - @Nullable - final String screenResolution; - private Context appContext; - - @NonNull - final String locale; - - @NonNull - final String[] cpuAbi; - - DeviceData(@NonNull Context appContext, @NonNull SharedPreferences sharedPref) { - screenDensity = getScreenDensity(appContext); - dpi = getScreenDensityDpi(appContext); - screenResolution = getScreenResolution(appContext); - this.appContext = appContext; - locale = getLocale(); - id = retrieveUniqueInstallId(sharedPref); - cpuAbi = getCpuAbi(); - freeMemory = calculateFreeMemory(); - totalMemory = calculateTotalMemory(); - freeDisk = calculateFreeDisk(); - orientation = calculateOrientation(appContext); - } - @Override public void toStream(@NonNull JsonStream writer) throws IOException { writer.beginObject(); @@ -90,24 +37,6 @@ public void toStream(@NonNull JsonStream writer) throws IOException { writer.endObject(); } - // TODO migrate metadata values to separate class - void addDeviceMetaData(MetaData metaData) { - metaData.addToTab("device", "batteryLevel", getBatteryLevel(appContext)); - metaData.addToTab("device", "charging", isCharging(appContext)); - metaData.addToTab("device", "locationStatus", getLocationStatus(appContext)); - metaData.addToTab("device", "networkAccess", getNetworkAccess(appContext)); - metaData.addToTab("device", "time", getTime()); - metaData.addToTab("device", "brand", Build.BRAND); - metaData.addToTab("device", "apiLevel", Build.VERSION.SDK_INT); - metaData.addToTab("device", "osBuild", Build.DISPLAY); - metaData.addToTab("device", "locale", locale); - metaData.addToTab("device", "screenDensity", screenDensity); - metaData.addToTab("device", "dpi", dpi); - metaData.addToTab("device", "emulator", isEmulator()); - metaData.addToTab("device", "screenResolution", screenResolution); - metaData.addToTab("device", "cpuAbi", cpuAbi); - } - /** * @return the device's unique ID for the current app installation */ @@ -170,7 +99,7 @@ public Long getFreeDisk() { * * @param freeDisk the new free disk space, in bytes */ - public void setFreeDisk(long freeDisk) { + public void setFreeDisk(@Nullable Long freeDisk) { this.freeDisk = freeDisk; } @@ -191,266 +120,4 @@ public void setOrientation(@Nullable String orientation) { this.orientation = orientation; } - - /** - * Guesses whether the current device is an emulator or not, erring on the side of caution - * - * @return true if the current device is an emulator - */ - private boolean isEmulator() { - String fingerprint = Build.FINGERPRINT; - return fingerprint.startsWith("unknown") - || fingerprint.contains("generic") - || fingerprint.contains("vbox"); // genymotion - } - - /** - * The screen density scaling factor of the current Android device - */ - @Nullable - private static Float getScreenDensity(@NonNull 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(@NonNull 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(@NonNull Context appContext) { - Resources resources = appContext.getResources(); - if (resources == null) { - return null; - } - DisplayMetrics metrics = resources.getDisplayMetrics(); - int max = Math.max(metrics.widthPixels, metrics.heightPixels); - int min = Math.min(metrics.widthPixels, metrics.heightPixels); - return String.format(Locale.US, "%dx%d", max, min); - } - - /** - * Get the total memory available on the current Android device, in bytes - */ - static long calculateTotalMemory() { - if (Runtime.getRuntime().maxMemory() != Long.MAX_VALUE) { - return Runtime.getRuntime().maxMemory(); - } else { - return Runtime.getRuntime().totalMemory(); - } - } - - /** - * Get the locale of the current Android device, eg en_US - */ - @NonNull - private static String getLocale() { - return Locale.getDefault().toString(); - } - - /** - * Get the unique id for the current app installation, creating a unique UUID if needed - */ - @Nullable - private String retrieveUniqueInstallId(@NonNull SharedPreferences sharedPref) { - String installId = sharedPref.getString(INSTALL_ID_KEY, null); - - if (installId == null) { - installId = UUID.randomUUID().toString(); - sharedPref.edit().putString(INSTALL_ID_KEY, installId).apply(); - } - return installId; - } - - /** - * Gets information about the CPU / API - */ - @NonNull - private static String[] getCpuAbi() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return SupportedAbiWrapper.getSupportedAbis(); - } - return Abi2Wrapper.getAbi1andAbi2(); - } - - /** - * Wrapper class to allow the test framework to use the correct version of the CPU / ABI - */ - private static class SupportedAbiWrapper { - @RequiresApi(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 { - @NonNull - public static String[] getAbi1andAbi2() { - return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; - } - } - - - /** - * Get the free disk space on the smallest disk - */ - @Nullable - private static Long calculateFreeDisk() { - try { - StatFs externalStat = new StatFs(Environment.getExternalStorageDirectory().getPath()); - long externalBytesAvailable = - (long) externalStat.getBlockSize() * (long) externalStat.getBlockCount(); - - StatFs internalStat = new StatFs(Environment.getDataDirectory().getPath()); - long internalBytesAvailable = - (long) internalStat.getBlockSize() * (long) internalStat.getBlockCount(); - - return Math.min(internalBytesAvailable, externalBytesAvailable); - } catch (Exception exception) { - Logger.warn("Could not get freeDisk"); - } - return null; - } - - /** - * Get the amount of memory remaining that the VM can allocate - */ - private static long calculateFreeMemory() { - Runtime runtime = Runtime.getRuntime(); - if (runtime.maxMemory() != Long.MAX_VALUE) { - return runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory(); - } else { - return runtime.freeMemory(); - } - } - - /** - * Get the device orientation, eg. "landscape" - */ - @Nullable - private static String calculateOrientation(@NonNull Context appContext) { - String orientation; - switch (appContext.getResources().getConfiguration().orientation) { - case android.content.res.Configuration.ORIENTATION_LANDSCAPE: - orientation = "landscape"; - break; - case android.content.res.Configuration.ORIENTATION_PORTRAIT: - orientation = "portrait"; - break; - default: - orientation = null; - break; - } - return orientation; - } - - /** - * Get the current battery charge level, eg 0.3 - */ - @Nullable - private static Float getBatteryLevel(@NonNull Context appContext) { - try { - IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - Intent batteryStatus = appContext.registerReceiver(null, ifilter); - - return batteryStatus.getIntExtra("level", -1) - / (float) batteryStatus.getIntExtra("scale", -1); - } catch (Exception exception) { - Logger.warn("Could not get batteryLevel"); - } - return null; - } - - /** - * Is the device currently charging/full battery? - */ - @Nullable - private static Boolean isCharging(@NonNull Context appContext) { - try { - IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); - Intent batteryStatus = appContext.registerReceiver(null, ifilter); - - int status = batteryStatus.getIntExtra("status", -1); - return (status == BatteryManager.BATTERY_STATUS_CHARGING - || status == BatteryManager.BATTERY_STATUS_FULL); - } catch (Exception exception) { - Logger.warn("Could not get charging status"); - } - return null; - } - - /** - * Get the current status of location services - */ - @Nullable - private static String getLocationStatus(@NonNull Context appContext) { - try { - ContentResolver cr = appContext.getContentResolver(); - String providersAllowed = - Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED); - if (providersAllowed != null && providersAllowed.length() > 0) { - return "allowed"; - } else { - return "disallowed"; - } - } catch (Exception exception) { - Logger.warn("Could not get locationStatus"); - } - return null; - } - - /** - * Get the current status of network access, eg "cellular" - */ - @Nullable - private static String getNetworkAccess(@NonNull Context appContext) { - try { - ConnectivityManager cm = - (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - if (activeNetwork != null && activeNetwork.isConnectedOrConnecting()) { - if (activeNetwork.getType() == 1) { - return "wifi"; - } else if (activeNetwork.getType() == 9) { - return "ethernet"; - } else { - // We default to cellular as the other enums are all cellular in some - // form or another - return "cellular"; - } - } else { - return "none"; - } - } catch (Exception exception) { - Logger.warn("Could not get network access information, we " - + "recommend granting the 'android.permission.ACCESS_NETWORK_STATE' permission"); - } - return null; - } - - /** - * Get the current time on the device, in ISO8601 format. - */ - @NonNull - private String getTime() { - return DateUtils.toIso8601(new Date()); - } - } diff --git a/sdk/src/main/java/com/bugsnag/android/DeviceDataCollector.java b/sdk/src/main/java/com/bugsnag/android/DeviceDataCollector.java new file mode 100644 index 0000000000..d9fe7b1075 --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/DeviceDataCollector.java @@ -0,0 +1,412 @@ +package com.bugsnag.android; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.os.Build; +import android.os.Environment; +import android.os.StatFs; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.RequiresApi; +import android.util.DisplayMetrics; + +import java.io.File; +import java.util.Date; +import java.util.Locale; +import java.util.UUID; + +class DeviceDataCollector { + + 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" + }; + + private static final String INSTALL_ID_KEY = "install.iud"; + + private final Client client; + private final boolean emulator; + private final Context appContext; + private final Resources resources; + private final DisplayMetrics displayMetrics; + private final String id; + + @Nullable + Float screenDensity; + + @Nullable + Integer dpi; + + @Nullable + String screenResolution; + + @NonNull + String locale; + + @NonNull + String[] cpuAbi; + + DeviceDataCollector(Client client) { + this.client = client; + this.appContext = client.appContext; + resources = appContext.getResources(); + + if (resources != null) { + displayMetrics = resources.getDisplayMetrics(); + } else { + displayMetrics = null; + } + + screenDensity = getScreenDensity(); + dpi = getScreenDensityDpi(); + screenResolution = getScreenResolution(); + locale = getLocale(); + cpuAbi = getCpuAbi(); + emulator = isEmulator(); + id = retrieveUniqueInstallId(); + } + + DeviceDataSummary generateDeviceDataSummary() { + DeviceDataSummary data = new DeviceDataSummary(); + populateDeviceDataSummary(data); + return data; + } + + DeviceData generateDeviceData() { + DeviceData data = new DeviceData(); + populateDeviceDataSummary(data); + data.setId(id); + data.setTotalMemory(calculateTotalMemory()); + data.setFreeMemory(calculateFreeMemory()); + data.setFreeDisk(calculateFreeDisk()); + data.setOrientation(calculateOrientation()); + return data; + } + + private void populateDeviceDataSummary(DeviceDataSummary data) { + data.setManufacturer(Build.MANUFACTURER); + data.setModel(Build.MODEL); + data.setOsName("android"); + data.setOsVersion(Build.VERSION.RELEASE); + data.setJailbroken(isRooted()); + } + + void populateDeviceMetaData(MetaData metaData) { + metaData.addToTab("device", "batteryLevel", getBatteryLevel()); + metaData.addToTab("device", "charging", isCharging()); + metaData.addToTab("device", "locationStatus", getLocationStatus()); + metaData.addToTab("device", "networkAccess", getNetworkAccess()); + metaData.addToTab("device", "time", getTime()); + metaData.addToTab("device", "brand", Build.BRAND); + metaData.addToTab("device", "apiLevel", Build.VERSION.SDK_INT); + metaData.addToTab("device", "osBuild", Build.DISPLAY); + metaData.addToTab("device", "locale", locale); + metaData.addToTab("device", "screenDensity", screenDensity); + metaData.addToTab("device", "dpi", dpi); + metaData.addToTab("device", "emulator", emulator); + metaData.addToTab("device", "screenResolution", screenResolution); + metaData.addToTab("device", "cpuAbi", cpuAbi); + } + + /** + * Check if the current Android device is rooted + */ + private 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 false; + } + return false; + } + + /** + * Guesses whether the current device is an emulator or not, erring on the side of caution + * + * @return true if the current device is an emulator + */ + private boolean isEmulator() { + String fingerprint = Build.FINGERPRINT; + return fingerprint.startsWith("unknown") + || fingerprint.contains("generic") + || fingerprint.contains("vbox"); // genymotion + } + + /** + * The screen density scaling factor of the current Android device + */ + @Nullable + private Float getScreenDensity() { + if (displayMetrics != null) { + return displayMetrics.density; + } else { + return null; + } + } + + /** + * The screen density of the current Android device in dpi, eg. 320 + */ + @Nullable + private Integer getScreenDensityDpi() { + if (displayMetrics != null) { + return displayMetrics.densityDpi; + } else { + return null; + } + } + + /** + * The screen resolution of the current Android device in px, eg. 1920x1080 + */ + @Nullable + private String getScreenResolution() { + if (displayMetrics != null) { + int max = Math.max(displayMetrics.widthPixels, displayMetrics.heightPixels); + int min = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels); + return String.format(Locale.US, "%dx%d", max, min); + } else { + return null; + } + } + + /** + * Get the total memory available on the current Android device, in bytes + */ + static long calculateTotalMemory() { + Runtime runtime = Runtime.getRuntime(); + if (runtime.maxMemory() != Long.MAX_VALUE) { + return runtime.maxMemory(); + } else { + return runtime.totalMemory(); + } + } + + /** + * Get the locale of the current Android device, eg en_US + */ + @NonNull + private String getLocale() { + return Locale.getDefault().toString(); + } + + /** + * Get the unique id for the current app installation, creating a unique UUID if needed + */ + @Nullable + private String retrieveUniqueInstallId() { + SharedPreferences sharedPref = client.sharedPrefs; + String installId = sharedPref.getString(INSTALL_ID_KEY, null); + + if (installId == null) { + installId = UUID.randomUUID().toString(); + sharedPref.edit().putString(INSTALL_ID_KEY, installId).apply(); + } + return installId; + } + + /** + * Gets information about the CPU / API + */ + @NonNull + private String[] getCpuAbi() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return SupportedAbiWrapper.getSupportedAbis(); + } + return Abi2Wrapper.getAbi1andAbi2(); + } + + /** + * Get the free disk space on the smallest disk + */ + @Nullable + private Long calculateFreeDisk() { + try { + StatFs externalStat = new StatFs(Environment.getExternalStorageDirectory().getPath()); + long externalBytesAvailable = + (long) externalStat.getBlockSize() * (long) externalStat.getBlockCount(); + + StatFs internalStat = new StatFs(Environment.getDataDirectory().getPath()); + long internalBytesAvailable = + (long) internalStat.getBlockSize() * (long) internalStat.getBlockCount(); + + return Math.min(internalBytesAvailable, externalBytesAvailable); + } catch (Exception exception) { + Logger.warn("Could not get freeDisk"); + } + return null; + } + + /** + * Get the amount of memory remaining that the VM can allocate + */ + private long calculateFreeMemory() { + Runtime runtime = Runtime.getRuntime(); + if (runtime.maxMemory() != Long.MAX_VALUE) { + return runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory(); + } else { + return runtime.freeMemory(); + } + } + + /** + * Get the device orientation, eg. "landscape" + */ + @Nullable + private String calculateOrientation() { + String orientation = null; + + if (resources != null) { + switch (resources.getConfiguration().orientation) { + case android.content.res.Configuration.ORIENTATION_LANDSCAPE: + orientation = "landscape"; + break; + case android.content.res.Configuration.ORIENTATION_PORTRAIT: + orientation = "portrait"; + break; + default: + break; + } + } + return orientation; + } + + /** + * Get the current battery charge level, eg 0.3 + */ + @Nullable + private Float getBatteryLevel() { + try { + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = appContext.registerReceiver(null, ifilter); + + return batteryStatus.getIntExtra("level", -1) + / (float) batteryStatus.getIntExtra("scale", -1); + } catch (Exception exception) { + Logger.warn("Could not get batteryLevel"); + } + return null; + } + + /** + * Is the device currently charging/full battery? + */ + @Nullable + private Boolean isCharging() { + try { + IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + Intent batteryStatus = appContext.registerReceiver(null, ifilter); + + int status = batteryStatus.getIntExtra("status", -1); + return (status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL); + } catch (Exception exception) { + Logger.warn("Could not get charging status"); + } + return null; + } + + /** + * Get the current status of location services + */ + @Nullable + private String getLocationStatus() { + try { + ContentResolver cr = appContext.getContentResolver(); + String providersAllowed = + Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED); + if (providersAllowed != null && providersAllowed.length() > 0) { + return "allowed"; + } else { + return "disallowed"; + } + } catch (Exception exception) { + Logger.warn("Could not get locationStatus"); + } + return null; + } + + /** + * Get the current status of network access, eg "cellular" + */ + @Nullable + private String getNetworkAccess() { + try { + ConnectivityManager cm = + (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + if (activeNetwork != null && activeNetwork.isConnectedOrConnecting()) { + if (activeNetwork.getType() == 1) { + return "wifi"; + } else if (activeNetwork.getType() == 9) { + return "ethernet"; + } else { + // We default to cellular as the other enums are all cellular in some + // form or another + return "cellular"; + } + } else { + return "none"; + } + } catch (Exception exception) { + Logger.warn("Could not get network access information, we " + + "recommend granting the 'android.permission.ACCESS_NETWORK_STATE' permission"); + } + return null; + } + + /** + * Get the current time on the device, in ISO8601 format. + */ + @NonNull + private String getTime() { + return DateUtils.toIso8601(new Date()); + } + + /** + * Wrapper class to allow the test framework to use the correct version of the CPU / ABI + */ + static class SupportedAbiWrapper { + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + static String[] getSupportedAbis() { + return Build.SUPPORTED_ABIS; + } + } + + /** + * Wrapper class to allow the test framework to use the correct version of the CPU / ABI + */ + static class Abi2Wrapper { + @NonNull + static String[] getAbi1andAbi2() { + return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; + } + } +} diff --git a/sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java b/sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java index 1a7c06fd17..b0a5dbc2f1 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java +++ b/sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java @@ -2,32 +2,28 @@ import android.support.annotation.NonNull; -import java.io.File; import java.io.IOException; public class DeviceDataSummary implements JsonStream.Streamable { - private boolean rooted = isRooted(); + private boolean rooted; + @SuppressWarnings("NullableProblems") // set after initialisation @NonNull private String manufacturer; + @SuppressWarnings("NullableProblems") // set after initialisation @NonNull private String model; + @SuppressWarnings("NullableProblems") // set after initialisation @NonNull private String osName; + @SuppressWarnings("NullableProblems") // set after initialisation @NonNull private String osVersion; - DeviceDataSummary() { - manufacturer = android.os.Build.MANUFACTURER; - model = android.os.Build.MODEL; - osName = "android"; - osVersion = android.os.Build.VERSION.RELEASE; - } - @Override public void toStream(@NonNull JsonStream writer) throws IOException { writer.beginObject(); @@ -128,40 +124,4 @@ public void setOsVersion(@NonNull String osVersion) { this.osVersion = osVersion; } - 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 - */ - 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 false; - } - return false; - } - } diff --git a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java index 006ba8b835..7e816a0438 100644 --- a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java @@ -120,29 +120,29 @@ public static String getDeviceId() { @NonNull public static String getDeviceLocale() { - return getClient().deviceData.locale; + return getClient().deviceDataCollector.locale; } public static double getDeviceTotalMemory() { - return DeviceData.calculateTotalMemory(); + return DeviceDataCollector.calculateTotalMemory(); } @Nullable public static Boolean getDeviceRooted() { - return DeviceDataSummary.isRooted(); + return getClient().deviceData.isJailbroken(); } public static float getDeviceScreenDensity() { - return getClient().deviceData.screenDensity; + return getClient().deviceDataCollector.screenDensity; } public static int getDeviceDpi() { - return getClient().deviceData.dpi; + return getClient().deviceDataCollector.dpi; } @Nullable public static String getDeviceScreenResolution() { - return getClient().deviceData.screenResolution; + return getClient().deviceDataCollector.screenResolution; } public static String getDeviceManufacturer() { @@ -171,7 +171,7 @@ public static int getDeviceApiLevel() { @NonNull public static String[] getDeviceCpuAbi() { - return getClient().deviceData.cpuAbi; + return getClient().deviceDataCollector.cpuAbi; }