From 7ef8a8c8bb8cabb1935019f837cde8d2a1d3d656 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 3 Jul 2018 11:23:14 +0100 Subject: [PATCH 1/4] refactor serialisation of app information to support unity --- .../bugsnag/android/AppDataSummaryTest.java | 37 +-- .../java/com/bugsnag/android/AppDataTest.java | 54 ++-- .../com/bugsnag/android/BugsnagTestUtils.java | 4 + .../java/com/bugsnag/android/ClientTest.java | 17 +- .../java/com/bugsnag/android/ErrorTest.java | 6 + .../android/SessionTrackingPayloadTest.java | 7 +- .../java/com/bugsnag/android/AppData.java | 237 +++++++++++++----- .../com/bugsnag/android/AppDataCollector.java | 217 ---------------- .../com/bugsnag/android/AppDataSummary.java | 128 ---------- .../main/java/com/bugsnag/android/Client.java | 43 ++-- .../main/java/com/bugsnag/android/Error.java | 8 +- .../java/com/bugsnag/android/ErrorStore.java | 3 +- .../java/com/bugsnag/android/JsonStream.java | 15 ++ .../java/com/bugsnag/android/MapUtils.java | 21 ++ .../java/com/bugsnag/android/MetaData.java | 83 +----- .../com/bugsnag/android/NativeInterface.java | 17 +- .../bugsnag/android/ObjectJsonStreamer.java | 81 ++++++ .../com/bugsnag/android/SessionTracker.java | 8 +- .../android/SessionTrackingPayload.java | 8 +- 19 files changed, 385 insertions(+), 609 deletions(-) delete mode 100644 sdk/src/main/java/com/bugsnag/android/AppDataCollector.java delete mode 100644 sdk/src/main/java/com/bugsnag/android/AppDataSummary.java create mode 100644 sdk/src/main/java/com/bugsnag/android/MapUtils.java create mode 100644 sdk/src/main/java/com/bugsnag/android/ObjectJsonStreamer.java diff --git a/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java b/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java index c331d0c980..4b7899cf73 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import static com.bugsnag.android.BugsnagTestUtils.generateClient; +import static com.bugsnag.android.BugsnagTestUtils.mapToJson; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -16,12 +17,13 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest public class AppDataSummaryTest { - private AppDataSummary appData; + private Map appData; /** * Configures a new AppDataSummary for testing accessors + serialisation @@ -30,8 +32,8 @@ public class AppDataSummaryTest { */ @Before public void setUp() throws Exception { - AppDataCollector appDataCollector = new AppDataCollector(generateClient()); - appData = appDataCollector.generateAppDataSummary(); + AppData appData = new AppData(generateClient()); + this.appData = appData.getAppDataSummary(); } @After @@ -41,47 +43,32 @@ public void tearDown() throws Exception { @Test public void testVersionCode() { - assertEquals(Integer.valueOf(1), appData.getVersionCode()); - int expected = 15; - appData.setVersionCode(expected); - assertEquals(Integer.valueOf(expected), appData.getVersionCode()); + assertEquals(1, appData.get("versionCode")); } @Test public void testVersionName() { - assertEquals("1.0", appData.getVersionName()); - String expected = "1.2.3"; - appData.setVersionName(expected); - assertEquals(expected, appData.getVersionName()); + assertEquals("1.0", appData.get("version")); } @Test public void testReleaseStage() { - assertEquals("development", appData.getReleaseStage()); - String expected = "beta"; - appData.setReleaseStage(expected); - assertEquals(expected, appData.getReleaseStage()); + assertEquals("development", appData.get("releaseStage")); } @Test public void testNotifierType() { - assertEquals("android", appData.getNotifierType()); - String expected = "custom"; - appData.setNotifierType(expected); - assertEquals(expected, appData.getNotifierType()); + assertEquals("android", appData.get("type")); } @Test public void testCodeBundleId() { - assertNull(appData.getCodeBundleId()); - String expected = "123"; - appData.setCodeBundleId(expected); - assertEquals(expected, appData.getCodeBundleId()); + assertNull(appData.get("codeBundleId")); } @Test - public void testJsonSerialisation() throws IOException, JSONException { - JSONObject appDataJson = streamableToJson(appData); + public void testJsonSerialisation() throws JSONException { + JSONObject appDataJson = mapToJson(appData); assertEquals(1, appDataJson.getInt("versionCode")); assertEquals("1.0", appDataJson.get("version")); assertEquals("development", appDataJson.get("releaseStage")); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java index e875425866..7933813096 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java @@ -1,7 +1,6 @@ package com.bugsnag.android; -import static com.bugsnag.android.BugsnagTestUtils.generateClient; -import static com.bugsnag.android.BugsnagTestUtils.generateSessionTracker; +import static com.bugsnag.android.BugsnagTestUtils.mapToJson; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -9,7 +8,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import android.content.Context; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -22,14 +20,15 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest public class AppDataTest { private Configuration config; - private AppData appData; - private AppDataCollector appDataCollector; + private Map appData; + private Client client; /** * Configures a new AppData for testing accessors + serialisation @@ -39,9 +38,8 @@ public class AppDataTest { @Before public void setUp() throws Exception { config = new Configuration("some-api-key"); - Client client = new Client(InstrumentationRegistry.getContext(), config); - appDataCollector = new AppDataCollector(client); - appData = appDataCollector.generateAppData(); + client = new Client(InstrumentationRegistry.getContext(), config); + appData = new AppData(client).getAppData(); } @After @@ -51,47 +49,33 @@ public void tearDown() throws Exception { @Test public void testPackageName() { - assertEquals("com.bugsnag.android.test", appData.getPackageName()); - String expected = "com.example.foo"; - appData.setPackageName(expected); - assertEquals(expected, appData.getPackageName()); + assertEquals("com.bugsnag.android.test", appData.get("packageName")); } @Test public void testBuildUuid() { - assertNull(appData.getBuildUuid()); - String expected = "fad4902f"; - appData.setBuildUuid(expected); - assertEquals(expected, appData.getBuildUuid()); + assertNull(appData.get("buildUUID")); } @Test public void testDuration() { - assertTrue(appData.getDuration() > 0); - long expected = 1500; - appData.setDuration(expected); - assertEquals(expected, appData.getDuration()); + assertTrue(((Long) appData.get("duration")) > 0); } @Test public void testDurationInForeground() { - assertEquals(0, appData.getDurationInForeground()); - long expected = 1500; - appData.setDurationInForeground(expected); - assertEquals(expected, appData.getDurationInForeground()); + assertEquals(0L, appData.get("durationInForeground")); } @Test public void testInForeground() { - assertFalse(appData.isInForeground()); - appData.setInForeground(true); - assertTrue(appData.isInForeground()); + assertFalse((Boolean) appData.get("inForeground")); } @Test - public void testJsonSerialisation() throws JSONException, IOException { - appData.setBuildUuid("fa54de"); - JSONObject appDataJson = streamableToJson(appData); + public void testJsonSerialisation() throws JSONException { + appData.put("buildUUID", "fa54de"); + JSONObject appDataJson = mapToJson(appData); assertEquals(1, appDataJson.getInt("versionCode")); assertEquals("1.0", appDataJson.get("version")); @@ -105,22 +89,20 @@ public void testJsonSerialisation() throws JSONException, IOException { } @Test - public void testAppVersionOverride() throws JSONException, IOException { + public void testAppVersionOverride() throws JSONException { String appVersion = "1.2.3"; config.setAppVersion(appVersion); - appData = appDataCollector.generateAppData(); - JSONObject appDataJson = streamableToJson(appData); + JSONObject appDataJson = mapToJson(client.appData.getAppData()); assertEquals(appVersion, appDataJson.get("version")); } @Test - public void testReleaseStageOverride() throws JSONException, IOException { + public void testReleaseStageOverride() throws JSONException { String releaseStage = "test-stage"; config.setReleaseStage(releaseStage); - appData = appDataCollector.generateAppData(); - JSONObject appDataJson = streamableToJson(appData); + JSONObject appDataJson = mapToJson(client.appData.getAppData()); assertEquals(releaseStage, appDataJson.get("releaseStage")); } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java b/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java index 59eaaa1356..5c53915208 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java @@ -26,6 +26,10 @@ private static String streamableToString(JsonStream.Streamable streamable) throw return writer.toString(); } + static JSONObject mapToJson(Map map) { + return new JSONObject(map); + } + static JSONObject streamableToJson(JsonStream.Streamable streamable) throws JSONException, IOException { return new JSONObject(streamableToString(streamable)); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java index b1d769eee6..b3e9953d5e 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -307,26 +307,23 @@ public void testAppData() { Client client = generateClient(); AppData appData = client.getAppData(); assertNotNull(appData); - assertNotEquals(client.appData, appData); - assertNotEquals(client.getAppData(), appData); + assertEquals(client.getAppData(), appData); } @Test public void testAppDataSummary() { Client client = generateClient(); - AppDataSummary appData = client.getAppDataSummary(); - assertNotNull(appData); - assertNotEquals(client.getAppDataSummary(), appData); + AppData appData = client.getAppData(); + + Map appDataSummary = appData.getAppDataSummary(); + assertNotNull(appDataSummary); + assertNotEquals(appData.getAppDataSummary(), appData); } @Test public void testAppDataMetaData() { Client client = generateClient(); - MetaData metaData = new MetaData(); - Map app = metaData.getTab("app"); - assertEquals(0, app.size()); - - client.populateAppMetaData(metaData); + Map app = client.getAppData().getAppDataMetaData(); assertEquals(5, app.size()); assertEquals("Bugsnag Android Tests", app.get("name")); assertEquals("com.bugsnag.android.test", app.get("packageName")); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java index ab6aad3414..1241684914 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java @@ -78,11 +78,17 @@ public void testGetExceptionMessage() { @Test public void testBasicSerialization() throws JSONException, IOException { + Client client = generateClient(); + error.setAppData(client.getAppData().getAppData()); + JSONObject errorJson = streamableToJson(error); assertEquals("warning", errorJson.get("severity")); assertNotNull(errorJson.get("severity")); + assertNotNull(errorJson.get("severityReason")); assertNotNull(errorJson.get("metaData")); assertNotNull(errorJson.get("threads")); + assertNotNull(errorJson.get("exceptions")); + assertNotNull(errorJson.get("app")); } @Test diff --git a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java index 77d5ad5c14..6098feabf9 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java @@ -2,7 +2,6 @@ import static com.bugsnag.android.BugsnagTestUtils.generateClient; import static com.bugsnag.android.BugsnagTestUtils.generateSession; -import static com.bugsnag.android.BugsnagTestUtils.generateSessionTracker; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -54,8 +53,8 @@ public void setUp() throws Exception { private SessionTrackingPayload generatePayloadFromSession(Context context, Session session) throws Exception { - appData = new AppDataCollector(generateClient()).generateAppData(); - return new SessionTrackingPayload(session, appData); + appData = generateClient().getAppData(); + return new SessionTrackingPayload(session, appData.getAppDataSummary()); } /** @@ -94,7 +93,7 @@ public void testMultipleSessionFiles() throws Exception { sessionStore.write(generateSession()); List storedFiles = sessionStore.findStoredFiles(); - SessionTrackingPayload payload = new SessionTrackingPayload(storedFiles, appData); + SessionTrackingPayload payload = new SessionTrackingPayload(storedFiles, appData.getAppDataSummary()); rootNode = streamableToJson(payload); assertNotNull(rootNode); diff --git a/sdk/src/main/java/com/bugsnag/android/AppData.java b/sdk/src/main/java/com/bugsnag/android/AppData.java index 8db6603ad6..210ccf5d08 100644 --- a/sdk/src/main/java/com/bugsnag/android/AppData.java +++ b/sdk/src/main/java/com/bugsnag/android/AppData.java @@ -1,118 +1,221 @@ package com.bugsnag.android; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.bugsnag.android.MapUtils.*; /** - * Information about the running Android app, including app name, version and release stage. + * Collects various data on the application state */ -public class AppData extends AppDataSummary { +class AppData { - @SuppressWarnings("NullableProblems") // initialised after construction - @NonNull - private String packageName; + private static final long startTimeMs = SystemClock.elapsedRealtime(); + + static final String RELEASE_STAGE_DEVELOPMENT = "development"; + static final String RELEASE_STAGE_PRODUCTION = "production"; + + private final Client client; + private final Context appContext; + + private final String packageName; @Nullable - private String buildUuid; - - private long durationMs; - private long foregroundMs; - private boolean inForeground; - - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject(); - serialiseMinimalAppData(writer); - writer.name("id").value(packageName); - writer.name("buildUUID").value(buildUuid); - writer.name("duration").value(durationMs); - writer.name("durationInForeground").value(foregroundMs); - writer.name("inForeground").value(inForeground); - writer.endObject(); + final String appName; + + @Nullable + private PackageInfo packageInfo; + + @Nullable + private ApplicationInfo applicationInfo; + + @Nullable + private PackageManager packageManager; + + AppData(Client client) { + this.client = client; + this.appContext = client.appContext; + + // cache values which are widely used, expensive to lookup, or unlikely to change + packageName = appContext.getPackageName(); + + try { + packageManager = appContext.getPackageManager(); + packageInfo = packageManager.getPackageInfo(packageName, 0); + applicationInfo = packageManager.getApplicationInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException exception) { + Logger.warn("Could not retrieve package/application information for " + packageName); + } + + appName = getAppName(); } - /** - * @return the application's package name - */ - @NonNull - public String getPackageName() { - return packageName; + Map getAppDataSummary() { + Map map = new ConcurrentHashMap<>(); + Configuration config = client.config; + putSafely("type", calculateNotifierType(config), map); + putSafely("releaseStage", guessReleaseStage(), map); + putSafely("version", calculateVersionName(), map); + putSafely("versionCode", calculateVersionCode(), map); + putSafely("codeBundleId", config.getCodeBundleId(), map); + return map; } - /** - * Overrides the application's default package name - * - * @param packageName the package name - */ - public void setPackageName(@NonNull String packageName) { - this.packageName = packageName; + Map getAppData() { + Map map = getAppDataSummary(); + putSafely("id", packageName, map); + putSafely("buildUUID", client.config.getBuildUUID(), map); + putSafely("duration", getDurationMs(), map); + putSafely("durationInForeground", calculateDurationInForeground(), map); + putSafely("inForeground", client.sessionTracker.isInForeground(), map); + putSafely("packageName", packageName, map); + return map; + } + + Map getAppDataMetaData() { + Map map = new ConcurrentHashMap<>(); + putSafely("name", appName, map); + putSafely("packageName", packageName, map); + putSafely("versionName", calculateVersionName(), map); + putSafely("activeScreen", getActiveScreenClass(), map); + putSafely("memoryUsage", getMemoryUsage(), map); + putSafely("lowMemory", isLowMemory(), map); + return map; } /** - * @return the application's bugsnag build UUID + * Get the time in milliseconds since Bugsnag was initialized, which is a + * good approximation for how long the app has been running. */ - @Nullable - public String getBuildUuid() { - return buildUuid; + static long getDurationMs() { + return SystemClock.elapsedRealtime() - startTimeMs; } /** - * Overrides the application's default bugsnag build UUID + * Calculates the duration the app has been in the foreground * - * @param buildUuid the bugsnag build UUID + * @return the duration in ms */ - public void setBuildUuid(@Nullable String buildUuid) { - this.buildUuid = buildUuid; + private long calculateDurationInForeground() { + long nowMs = System.currentTimeMillis(); + return client.sessionTracker.getDurationInForegroundMs(nowMs); + } + + private String getActiveScreenClass() { + return client.sessionTracker.getContextActivity(); + } + + @NonNull + private String calculateNotifierType(Configuration config) { + String notifierType = config.getNotifierType(); + + if (notifierType != null) { + return notifierType; + } else { + return "android"; + } } /** - * @return the duration in ms for which the app has been running + * The version code of the running Android app, from android:versionCode + * in AndroidManifest.xml */ - public long getDuration() { - return durationMs; + @Nullable + private Integer calculateVersionCode() { + if (packageInfo != null) { + return packageInfo.versionCode; + } else { + return null; + } } /** - * Overrides the duration in ms for which the app has been running - * - * @param durationMs the new duration in ms + * The version code of the running Android app, from android:versionName + * in AndroidManifest.xml */ - public void setDuration(long durationMs) { - this.durationMs = durationMs; + @Nullable + private String calculateVersionName() { + String configAppVersion = client.config.getAppVersion(); + + if (configAppVersion != null) { + return configAppVersion; + } else if (packageInfo != null) { + return packageInfo.versionName; + } else { + return null; + } } /** - * @return the duration in ms for which the app has been running in the foreground + * Guess the release stage of the running Android app by checking the + * android:debuggable flag from AndroidManifest.xml. If the release stage was set in + * {@link Configuration}, this value will be returned instead. */ - public long getDurationInForeground() { - return foregroundMs; + @NonNull + String guessReleaseStage() { + String configStage = client.config.getReleaseStage(); + + if (configStage != null) { + return configStage; + } + if (applicationInfo != null) { + if ((applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + return RELEASE_STAGE_DEVELOPMENT; + } + } + return RELEASE_STAGE_PRODUCTION; } /** - * Overrides the duration in ms for which the app has been running in the foreground - * - * @param foregroundMs the new duration in ms + * The name of the running Android app, from android:label in + * AndroidManifest.xml */ - public void setDurationInForeground(long foregroundMs) { - this.foregroundMs = foregroundMs; + @Nullable + private String getAppName() { + if (packageManager != null && applicationInfo != null) { + return packageManager.getApplicationLabel(applicationInfo).toString(); + } else { + return null; + } } /** - * @return whether the app is in the foreground or not + * Get the actual memory used by the VM (which may not be the total used + * by the app in the case of NDK usage). */ - public boolean isInForeground() { - return inForeground; + private long getMemoryUsage() { + Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); } /** - * Overrides whether the app is in the foreground or not - * - * @param inForeground whether the app is in the foreground + * Check if the device is currently running low on memory. */ - public void setInForeground(boolean inForeground) { - this.inForeground = inForeground; + @Nullable + private Boolean isLowMemory() { + try { + ActivityManager activityManager = + (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); + + if (activityManager != null) { + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + activityManager.getMemoryInfo(memInfo); + return memInfo.lowMemory; + } + } catch (Exception exception) { + Logger.warn("Could not check lowMemory status"); + } + return null; } } diff --git a/sdk/src/main/java/com/bugsnag/android/AppDataCollector.java b/sdk/src/main/java/com/bugsnag/android/AppDataCollector.java deleted file mode 100644 index e77b6ef244..0000000000 --- a/sdk/src/main/java/com/bugsnag/android/AppDataCollector.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.bugsnag.android; - -import android.app.ActivityManager; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.SystemClock; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -/** - * Collects various data on the application state - */ -class AppDataCollector { - - private static final long startTimeMs = SystemClock.elapsedRealtime(); - - static final String RELEASE_STAGE_DEVELOPMENT = "development"; - static final String RELEASE_STAGE_PRODUCTION = "production"; - - private final Client client; - private final Context appContext; - - private final String packageName; - - @Nullable - final String appName; - - @Nullable - private PackageInfo packageInfo; - - @Nullable - private ApplicationInfo applicationInfo; - - @Nullable - private PackageManager packageManager; - - AppDataCollector(Client client) { - this.client = client; - this.appContext = client.appContext; - - // cache values which are widely used, expensive to lookup, or unlikely to change - packageName = appContext.getPackageName(); - - try { - packageManager = appContext.getPackageManager(); - packageInfo = packageManager.getPackageInfo(packageName, 0); - applicationInfo = packageManager.getApplicationInfo(packageName, 0); - } catch (PackageManager.NameNotFoundException exception) { - Logger.warn("Could not retrieve package/application information for " + packageName); - } - - appName = getAppName(); - } - - AppDataSummary generateAppDataSummary() { - AppDataSummary data = new AppDataSummary(); - populateAppDataSummary(data); - return data; - } - - AppData generateAppData() { - AppData data = new AppData(); - populateAppDataSummary(data); - data.setPackageName(packageName); - data.setBuildUuid(client.config.getBuildUUID()); - data.setDuration(getDurationMs()); - data.setDurationInForeground(calculateDurationInForeground()); - data.setInForeground(client.sessionTracker.isInForeground()); - return data; - } - - void populateAppMetaData(MetaData metaData) { - metaData.addToTab("app", "name", appName); - metaData.addToTab("app", "packageName", packageName); - metaData.addToTab("app", "versionName", calculateVersionName()); - metaData.addToTab("app", "activeScreen", getActiveScreenClass()); - metaData.addToTab("app", "memoryUsage", getMemoryUsage()); - metaData.addToTab("app", "lowMemory", isLowMemory()); - } - - private void populateAppDataSummary(AppDataSummary data) { - Configuration config = client.config; - data.setVersionCode(calculateVersionCode()); - data.setCodeBundleId(config.getCodeBundleId()); - data.setNotifierType(calculateNotifierType(config)); - data.setReleaseStage(guessReleaseStage()); - data.setVersionName(calculateVersionName()); - } - - /** - * Get the time in milliseconds since Bugsnag was initialized, which is a - * good approximation for how long the app has been running. - */ - static long getDurationMs() { - return SystemClock.elapsedRealtime() - startTimeMs; - } - - /** - * Calculates the duration the app has been in the foreground - * - * @return the duration in ms - */ - private long calculateDurationInForeground() { - long nowMs = System.currentTimeMillis(); - return client.sessionTracker.getDurationInForegroundMs(nowMs); - } - - private String getActiveScreenClass() { - return client.sessionTracker.getContextActivity(); - } - - @NonNull - private String calculateNotifierType(Configuration config) { - String notifierType = config.getNotifierType(); - - if (notifierType != null) { - return notifierType; - } else { - return "android"; - } - } - - /** - * The version code of the running Android app, from android:versionCode - * in AndroidManifest.xml - */ - @Nullable - private Integer calculateVersionCode() { - if (packageInfo != null) { - return packageInfo.versionCode; - } else { - return null; - } - } - - /** - * The version code of the running Android app, from android:versionName - * in AndroidManifest.xml - */ - @Nullable - private String calculateVersionName() { - String configAppVersion = client.config.getAppVersion(); - - if (configAppVersion != null) { - return configAppVersion; - } else if (packageInfo != null) { - return packageInfo.versionName; - } else { - return null; - } - } - - /** - * Guess the release stage of the running Android app by checking the - * android:debuggable flag from AndroidManifest.xml. If the release stage was set in - * {@link Configuration}, this value will be returned instead. - */ - @NonNull - String guessReleaseStage() { - String configStage = client.config.getReleaseStage(); - - if (configStage != null) { - return configStage; - } - if (applicationInfo != null) { - if ((applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { - return RELEASE_STAGE_DEVELOPMENT; - } - } - return RELEASE_STAGE_PRODUCTION; - } - - /** - * The name of the running Android app, from android:label in - * AndroidManifest.xml - */ - @Nullable - private String getAppName() { - if (packageManager != null && applicationInfo != null) { - return packageManager.getApplicationLabel(applicationInfo).toString(); - } else { - return null; - } - } - - /** - * Get the actual memory used by the VM (which may not be the total used - * by the app in the case of NDK usage). - */ - private long getMemoryUsage() { - Runtime runtime = Runtime.getRuntime(); - return runtime.totalMemory() - runtime.freeMemory(); - } - - /** - * Check if the device is currently running low on memory. - */ - @Nullable - private Boolean isLowMemory() { - try { - ActivityManager activityManager = - (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); - - if (activityManager != null) { - ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); - activityManager.getMemoryInfo(memInfo); - return memInfo.lowMemory; - } - } catch (Exception exception) { - Logger.warn("Could not check lowMemory status"); - } - return null; - } - -} diff --git a/sdk/src/main/java/com/bugsnag/android/AppDataSummary.java b/sdk/src/main/java/com/bugsnag/android/AppDataSummary.java deleted file mode 100644 index db5e554b6c..0000000000 --- a/sdk/src/main/java/com/bugsnag/android/AppDataSummary.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.bugsnag.android; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.IOException; - -/** - * Information about the running Android app which doesn't change over time, - * including app name, version and release stage. - *

- * App information in this class is cached during construction for faster - * subsequent lookups and to reduce GC overhead. - */ -public class AppDataSummary implements JsonStream.Streamable { - - @Nullable - private Integer versionCode; - - @Nullable - private String versionName; - - @SuppressWarnings("NullableProblems") // initialised after construction - @NonNull - private String releaseStage; - - @SuppressWarnings("NullableProblems") // initialised after construction - @NonNull - private String notifierType; - - @Nullable - private String codeBundleId; - - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject(); - serialiseMinimalAppData(writer); - writer.endObject(); - } - - void serialiseMinimalAppData(@NonNull JsonStream writer) throws IOException { - writer - .name("type").value(notifierType) - .name("releaseStage").value(releaseStage) - .name("version").value(versionName) - .name("versionCode").value(versionCode) - .name("codeBundleId").value(codeBundleId); - } - - /** - * @return the application's version code - */ - @Nullable - public Integer getVersionCode() { - return versionCode; - } - - /** - * Overrides the application's default version code - * - * @param versionCode the version code - */ - public void setVersionCode(@Nullable Integer versionCode) { - this.versionCode = versionCode; - } - - /** - * @return the application's version name - */ - @Nullable - public String getVersionName() { - return versionName; - } - - /** - * Overrides the application's default version name - * - * @param versionName the version name - */ - public void setVersionName(@Nullable String versionName) { - this.versionName = versionName; - } - - /** - * @return the application's release stage - */ - @NonNull - public String getReleaseStage() { - return releaseStage; - } - - /** - * Overrides the application's default release stage - * - * @param releaseStage the release stage - */ - public void setReleaseStage(@NonNull String releaseStage) { - this.releaseStage = releaseStage; - } - - @NonNull - @InternalApi - public String getNotifierType() { - return notifierType; - } - - @InternalApi - public void setNotifierType(@NonNull String notifierType) { - this.notifierType = notifierType; - } - - /** - * @return the application's code bundle ID, if it exists - */ - @Nullable - public String getCodeBundleId() { - return codeBundleId; - } - - /** - * Overrides the application's default code bundle ID - * - * @param codeBundleId the code bundle ID - */ - public void setCodeBundleId(@Nullable String codeBundleId) { - this.codeBundleId = codeBundleId; - } -} diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index e5d29d25bd..d093b990d9 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -25,6 +25,8 @@ import java.util.Observer; import java.util.concurrent.RejectedExecutionException; +import static com.bugsnag.android.MapUtils.getStringFromMap; + /** * A Bugsnag Client instance allows you to use Bugsnag in your Android app. * Typically you'd instead use the static access provided in the Bugsnag class. @@ -64,9 +66,6 @@ public class Client extends Observable implements Observer { protected final Configuration config; final Context appContext; - @NonNull - protected final AppData appData; - @NonNull protected final DeviceData deviceData; @@ -86,7 +85,7 @@ public class Client extends Observable implements Observer { private final EventReceiver eventReceiver; final SessionTracker sessionTracker; SharedPreferences sharedPrefs; - AppDataCollector appDataCollector; + AppData appData; /** * Initialize a Bugsnag client @@ -148,9 +147,7 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat // Set up and collect constant app and device diagnostics sharedPrefs = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); - appDataCollector = new AppDataCollector(this); - - appData = appDataCollector.generateAppData(); + appData = new AppData(this); deviceDataCollector = new DeviceDataCollector(this); deviceData = deviceDataCollector.generateDeviceData(); @@ -221,8 +218,8 @@ public void run() { config.addObserver(this); - boolean isNotProduction = !AppDataCollector.RELEASE_STAGE_PRODUCTION.equals( - appDataCollector.guessReleaseStage()); + boolean isNotProduction = !AppData.RELEASE_STAGE_PRODUCTION.equals( + appData.guessReleaseStage()); Logger.setEnabled(isNotProduction); @@ -499,7 +496,7 @@ public void setProjectPackages(String... projectPackages) { */ public void setReleaseStage(String releaseStage) { config.setReleaseStage(releaseStage); - Logger.setEnabled(!AppDataCollector.RELEASE_STAGE_PRODUCTION.equals(releaseStage)); + Logger.setEnabled(!AppData.RELEASE_STAGE_PRODUCTION.equals(releaseStage)); } /** @@ -567,18 +564,7 @@ public Collection getBreadcrumbs() { @NonNull @InternalApi public AppData getAppData() { - return appDataCollector.generateAppData(); - } - - @NonNull - @InternalApi - public AppDataSummary getAppDataSummary() { - return appDataCollector.generateAppDataSummary(); - } - - @InternalApi - public void populateAppMetaData(@NonNull MetaData metaData) { - appDataCollector.populateAppMetaData(metaData); + return appData; } @NonNull @@ -941,10 +927,12 @@ void notify(@NonNull Error error, } // generate new object each time, as this can be mutated by end-users - AppData errorAppData = appDataCollector.generateAppData(); + Map errorAppData = appData.getAppData(); // Don't notify unless releaseStage is in notifyReleaseStages - if (!config.shouldNotifyForReleaseStage(errorAppData.getReleaseStage())) { + String releaseStage = getStringFromMap("releaseStage", errorAppData); + + if (!config.shouldNotifyForReleaseStage(releaseStage)) { return; } @@ -953,10 +941,9 @@ void notify(@NonNull Error error, error.setDeviceData(errorDeviceData); deviceDataCollector.populateDeviceMetaData(error.getMetaData()); - error.setAppData(errorAppData); - // add additional info that belongs in metadata - appDataCollector.populateAppMetaData(error.getMetaData()); + error.setAppData(errorAppData); + error.getMetaData().store.put("app", error.getMetaData()); // Attach breadcrumbs to the error error.setBreadcrumbs(breadcrumbs); @@ -1454,7 +1441,7 @@ public Configuration getConfig() { * @return the ms since the java epoch */ public long getLaunchTimeMs() { - return AppDataCollector.getDurationMs(); + return AppData.getDurationMs(); } } diff --git a/sdk/src/main/java/com/bugsnag/android/Error.java b/sdk/src/main/java/com/bugsnag/android/Error.java index b3bd97a32f..13ea13ac69 100644 --- a/sdk/src/main/java/com/bugsnag/android/Error.java +++ b/sdk/src/main/java/com/bugsnag/android/Error.java @@ -20,7 +20,7 @@ public class Error implements JsonStream.Streamable { @SuppressWarnings("NullableProblems") // set after construction @NonNull - private AppData appData; + private Map appData; @SuppressWarnings("NullableProblems") // set after construction @NonNull @@ -348,12 +348,12 @@ public void setDeviceId(@Nullable String id) { } /** - * Retrieves the {@link AppData} associated with this error + * Retrieves the map of data associated with this error * * @return the app metadata */ @NonNull - public AppData getAppData() { + Map getAppData() { return appData; } /** @@ -367,7 +367,7 @@ public DeviceData getDeviceData() { return deviceData; } - void setAppData(@NonNull AppData appData) { + void setAppData(@NonNull Map appData) { this.appData = appData; } diff --git a/sdk/src/main/java/com/bugsnag/android/ErrorStore.java b/sdk/src/main/java/com/bugsnag/android/ErrorStore.java index 5acbac0230..a8cfc4fe03 100644 --- a/sdk/src/main/java/com/bugsnag/android/ErrorStore.java +++ b/sdk/src/main/java/com/bugsnag/android/ErrorStore.java @@ -2,7 +2,6 @@ import android.content.Context; import android.support.annotation.NonNull; -import android.util.Log; import java.io.File; import java.util.ArrayList; @@ -165,7 +164,7 @@ private List findLaunchCrashReports(Collection storedFiles) { @NonNull @Override String getFilename(Error error) { - boolean isStartupCrash = isStartupCrash(AppDataCollector.getDurationMs()); + boolean isStartupCrash = isStartupCrash(AppData.getDurationMs()); String suffix = isStartupCrash ? STARTUP_CRASH : ""; return String.format(Locale.US, "%s%d_%s%s.json", storeDirectory, System.currentTimeMillis(), UUID.randomUUID().toString(), suffix); diff --git a/sdk/src/main/java/com/bugsnag/android/JsonStream.java b/sdk/src/main/java/com/bugsnag/android/JsonStream.java index 64a7049543..e975b1bdd6 100644 --- a/sdk/src/main/java/com/bugsnag/android/JsonStream.java +++ b/sdk/src/main/java/com/bugsnag/android/JsonStream.java @@ -10,8 +10,14 @@ import java.io.InputStreamReader; import java.io.Reader; import java.io.Writer; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; public class JsonStream extends JsonWriter { + + private final ObjectJsonStreamer objectJsonStreamer; + public interface Streamable { void toStream(@NonNull JsonStream stream) throws IOException; } @@ -27,6 +33,7 @@ public JsonStream(Writer out) { super(out); setSerializeNulls(false); this.out = out; + objectJsonStreamer = new ObjectJsonStreamer(); } // Allow chaining name().value() @@ -48,6 +55,14 @@ public void value(@Nullable Streamable streamable) throws IOException { streamable.toStream(this); } + /** + * Serialises an arbitrary object as JSON, handling primitive types as well as + * Collections, Maps, and arrays. + */ + public void value(@NonNull Object object) throws IOException { + objectJsonStreamer.objectToStream(object, this); + } + /** * Writes a File (its content) into the stream */ diff --git a/sdk/src/main/java/com/bugsnag/android/MapUtils.java b/sdk/src/main/java/com/bugsnag/android/MapUtils.java new file mode 100644 index 0000000000..aada556c09 --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/MapUtils.java @@ -0,0 +1,21 @@ +package com.bugsnag.android; + +import android.support.annotation.Nullable; + +import java.util.Map; + +final class MapUtils { + + @Nullable + static String getStringFromMap(String key, Map map) { + Object packageName = map.get(key); + return packageName instanceof String ? (String) packageName : null; + } + + static void putSafely(String key, @Nullable Object nullableObj, Map map) { + if (nullableObj != null) { + map.put(key, nullableObj); + } + } + +} diff --git a/sdk/src/main/java/com/bugsnag/android/MetaData.java b/sdk/src/main/java/com/bugsnag/android/MetaData.java index 258aba7797..104d32fdcb 100644 --- a/sdk/src/main/java/com/bugsnag/android/MetaData.java +++ b/sdk/src/main/java/com/bugsnag/android/MetaData.java @@ -22,19 +22,16 @@ * Diagnostic information is presented on your Bugsnag dashboard in tabs. */ public class MetaData extends Observable implements JsonStream.Streamable { - private static final String FILTERED_PLACEHOLDER = "[FILTERED]"; - private static final String OBJECT_PLACEHOLDER = "[OBJECT]"; - - private String[] filters = {"password"}; @NonNull final Map store; + final ObjectJsonStreamer jsonStreamer; /** * Create an empty MetaData object. */ public MetaData() { - store = new ConcurrentHashMap<>(); + this(new ConcurrentHashMap()); } /** @@ -42,11 +39,12 @@ public MetaData() { */ public MetaData(@NonNull Map map) { store = new ConcurrentHashMap<>(map); + jsonStreamer = new ObjectJsonStreamer(); } @Override public void toStream(@NonNull JsonStream writer) throws IOException { - objectToStream(store, writer); + jsonStreamer.objectToStream(store, writer); } /** @@ -114,13 +112,13 @@ Map getTab(String tabName) { } void setFilters(String... filters) { - this.filters = filters; + jsonStreamer.filters = filters; notifyBugsnagObservers(NotifyType.FILTERS); } String[] getFilters() { - return filters; + return jsonStreamer.filters; } @NonNull @@ -131,14 +129,14 @@ static MetaData merge(@NonNull MetaData... metaDataList) { if (metaData != null) { stores.add(metaData.store); - if (metaData.filters != null) { - filters.addAll(Arrays.asList(metaData.filters)); + if (metaData.jsonStreamer.filters != null) { + filters.addAll(Arrays.asList(metaData.jsonStreamer.filters)); } } } MetaData newMeta = new MetaData(mergeMaps(stores.toArray(new Map[0]))); - newMeta.filters = filters.toArray(new String[filters.size()]); + newMeta.setFilters(filters.toArray(new String[filters.size()])); return newMeta; } @@ -180,69 +178,6 @@ private static Map mergeMaps(@NonNull Map... map return result; } - // Write complex/nested values to a JsonStreamer - private void objectToStream(@Nullable Object obj, - @NonNull JsonStream writer) throws IOException { - if (obj == null) { - writer.nullValue(); - } else if (obj instanceof String) { - writer.value((String) obj); - } else if (obj instanceof Number) { - writer.value((Number) obj); - } else if (obj instanceof Boolean) { - writer.value((Boolean) obj); - } else if (obj instanceof Map) { - // Map objects - writer.beginObject(); - for (Object o : ((Map) obj).entrySet()) { - Map.Entry entry = (Map.Entry) o; - Object keyObj = entry.getKey(); - if (keyObj instanceof String) { - String key = (String) keyObj; - writer.name(key); - if (shouldFilter(key)) { - writer.value(FILTERED_PLACEHOLDER); - } else { - objectToStream(entry.getValue(), writer); - } - } - } - writer.endObject(); - } else if (obj instanceof Collection) { - // Collection objects (Lists, Sets etc) - writer.beginArray(); - for (Object entry : (Collection) obj) { - objectToStream(entry, writer); - } - writer.endArray(); - } else if (obj.getClass().isArray()) { - // Primitive array objects - writer.beginArray(); - int length = Array.getLength(obj); - for (int i = 0; i < length; i += 1) { - objectToStream(Array.get(obj, i), writer); - } - writer.endArray(); - } else { - writer.value(OBJECT_PLACEHOLDER); - } - } - - // Should this key be filtered - private boolean shouldFilter(@Nullable String key) { - if (filters == null || key == null) { - return false; - } - - for (String filter : filters) { - if (key.contains(filter)) { - return true; - } - } - - return false; - } - private void notifyBugsnagObservers(@NonNull NotifyType type) { setChanged(); super.notifyObservers(type.getValue()); diff --git a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java index 7e816a0438..24775cadad 100644 --- a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java @@ -8,6 +8,8 @@ import java.util.Map; import java.util.Observer; +import static com.bugsnag.android.MapUtils.getStringFromMap; + /** * Used as the entry point for native code to allow proguard to obfuscate other areas if needed */ @@ -80,23 +82,24 @@ public static String getUserName() { return getClient().getUser().getName(); } - @NonNull + @Nullable public static String getPackageName() { - return getClient().appData.getPackageName(); + return getStringFromMap("packageName", getClient().appData.getAppData()); } @Nullable public static String getAppName() { - return getClient().appDataCollector.appName; + return getClient().appData.appName; } @Nullable public static String getVersionName() { - return getClient().appData.getVersionName(); + return getStringFromMap("version", getClient().appData.getAppData()); } public static int getVersionCode() { - return getClient().appData.getVersionCode(); + Object versionCode = getClient().appData.getAppData().get("versionCode"); + return versionCode instanceof Integer ? (Integer) versionCode : -1; } @SuppressWarnings("checkstyle:AbbreviationAsWordInName") @@ -106,11 +109,11 @@ public static String getBuildUUID() { @Nullable public static String getAppVersion() { - return getClient().appData.getVersionName(); + return getStringFromMap("version", getClient().appData.getAppData()); } public static String getReleaseStage() { - return getClient().appData.getReleaseStage(); + return getStringFromMap("releaseStage", getClient().appData.getAppData()); } @Nullable diff --git a/sdk/src/main/java/com/bugsnag/android/ObjectJsonStreamer.java b/sdk/src/main/java/com/bugsnag/android/ObjectJsonStreamer.java new file mode 100644 index 0000000000..f6c7ec3eb4 --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/ObjectJsonStreamer.java @@ -0,0 +1,81 @@ +package com.bugsnag.android; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Map; + +class ObjectJsonStreamer { + + private static final String FILTERED_PLACEHOLDER = "[FILTERED]"; + private static final String OBJECT_PLACEHOLDER = "[OBJECT]"; + + String[] filters = {"password"}; + + // Write complex/nested values to a JsonStreamer + void objectToStream(@Nullable Object obj, + @NonNull JsonStream writer) throws IOException { + if (obj == null) { + writer.nullValue(); + } else if (obj instanceof String) { + writer.value((String) obj); + } else if (obj instanceof Number) { + writer.value((Number) obj); + } else if (obj instanceof Boolean) { + writer.value((Boolean) obj); + } else if (obj instanceof Map) { + // Map objects + writer.beginObject(); + for (Object o : ((Map) obj).entrySet()) { + Map.Entry entry = (Map.Entry) o; + Object keyObj = entry.getKey(); + if (keyObj instanceof String) { + String key = (String) keyObj; + writer.name(key); + if (shouldFilter(key)) { + writer.value(FILTERED_PLACEHOLDER); + } else { + objectToStream(entry.getValue(), writer); + } + } + } + writer.endObject(); + } else if (obj instanceof Collection) { + // Collection objects (Lists, Sets etc) + writer.beginArray(); + for (Object entry : (Collection) obj) { + objectToStream(entry, writer); + } + writer.endArray(); + } else if (obj.getClass().isArray()) { + // Primitive array objects + writer.beginArray(); + int length = Array.getLength(obj); + for (int i = 0; i < length; i += 1) { + objectToStream(Array.get(obj, i), writer); + } + writer.endArray(); + } else { + writer.value(OBJECT_PLACEHOLDER); + } + } + + // Should this key be filtered + private boolean shouldFilter(@Nullable String key) { + if (filters == null || key == null) { + return false; + } + + for (String filter : filters) { + if (key.contains(filter)) { + return true; + } + } + + return false; + } + +} diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index 4d1ec2cc33..d46df6d358 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -19,6 +19,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; +import static com.bugsnag.android.MapUtils.getStringFromMap; + class SessionTracker implements Application.ActivityLifecycleCallbacks { private static final String KEY_LIFECYCLE_CALLBACK = "ActivityLifecycle"; @@ -92,7 +94,7 @@ public void run() { flushStoredSessions(); SessionTrackingPayload payload = - new SessionTrackingPayload(session, client.appData); + new SessionTrackingPayload(session, client.appData.getAppDataSummary()); try { configuration.getDelivery().deliver(payload, configuration); @@ -123,7 +125,7 @@ void onAutoCaptureEnabled() { } private String getReleaseStage() { - return client.appData.getReleaseStage(); + return getStringFromMap("releaseStage", client.appData.getAppDataSummary()); } @Nullable @@ -157,7 +159,7 @@ void flushStoredSessions() { if (!storedFiles.isEmpty()) { SessionTrackingPayload payload = - new SessionTrackingPayload(storedFiles, client.appData); + new SessionTrackingPayload(storedFiles, client.appData.getAppDataSummary()); //FUTURE:SM Reduce duplication here and above try { diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java b/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java index 777c01306c..1e06854b00 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java @@ -7,23 +7,24 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; public class SessionTrackingPayload implements JsonStream.Streamable { private final Notifier notifier; private final Session session; private final DeviceDataSummary deviceDataSummary = new DeviceDataSummary(); - private final AppData appData; + private final Map appData; private final List files; - SessionTrackingPayload(List files, AppData appData) { + SessionTrackingPayload(List files, Map appData) { this.appData = appData; this.notifier = Notifier.getInstance(); this.session = null; this.files = files; } - SessionTrackingPayload(Session session, AppData appDataSummary) { + SessionTrackingPayload(Session session, Map appDataSummary) { this.appData = appDataSummary; this.notifier = Notifier.getInstance(); this.session = session; @@ -36,7 +37,6 @@ public void toStream(@NonNull JsonStream writer) throws IOException { writer.name("notifier").value(notifier); writer.name("app").value(appData); writer.name("device").value(deviceDataSummary); - writer.name("sessions").beginArray(); if (session == null) { From 0a8aa71f281e4a1c057a45ff50fbadb2c2c0cdbe Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 3 Jul 2018 12:03:46 +0100 Subject: [PATCH 2/4] refactor serialisation of device information to support unity --- .../java/com/bugsnag/android/ClientTest.java | 67 +-- .../android/DeviceDataSummaryTest.java | 32 +- .../com/bugsnag/android/DeviceDataTest.java | 40 +- .../java/com/bugsnag/android/ErrorTest.java | 9 +- .../android/SessionTrackingPayloadTest.java | 9 +- .../java/com/bugsnag/android/AppData.java | 38 +- .../main/java/com/bugsnag/android/Client.java | 33 +- .../java/com/bugsnag/android/DeviceData.java | 410 ++++++++++++++--- .../bugsnag/android/DeviceDataCollector.java | 412 ------------------ .../bugsnag/android/DeviceDataSummary.java | 127 ------ .../main/java/com/bugsnag/android/Error.java | 8 +- .../java/com/bugsnag/android/MapUtils.java | 6 - .../com/bugsnag/android/NativeInterface.java | 19 +- .../com/bugsnag/android/SessionTracker.java | 4 +- .../android/SessionTrackingPayload.java | 23 +- 15 files changed, 456 insertions(+), 781 deletions(-) delete mode 100644 sdk/src/main/java/com/bugsnag/android/DeviceDataCollector.java delete mode 100644 sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java index b3e9953d5e..adad8ba536 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -303,28 +303,17 @@ public void testBreadcrumbStoreNotModified() { } @Test - public void testAppData() { + public void testAppDataCollection() { Client client = generateClient(); AppData appData = client.getAppData(); - assertNotNull(appData); assertEquals(client.getAppData(), appData); } - @Test - public void testAppDataSummary() { - Client client = generateClient(); - AppData appData = client.getAppData(); - - Map appDataSummary = appData.getAppDataSummary(); - assertNotNull(appDataSummary); - assertNotEquals(appData.getAppDataSummary(), appData); - } - @Test public void testAppDataMetaData() { Client client = generateClient(); Map app = client.getAppData().getAppDataMetaData(); - assertEquals(5, app.size()); + assertEquals(6, app.size()); assertEquals("Bugsnag Android Tests", app.get("name")); assertEquals("com.bugsnag.android.test", app.get("packageName")); assertEquals("1.0", app.get("versionName")); @@ -333,45 +322,31 @@ public void testAppDataMetaData() { } @Test - public void testDeviceData() { + public void testDeviceDataCollection() { Client client = generateClient(); DeviceData deviceData = client.getDeviceData(); - assertNotNull(deviceData); - assertNotEquals(client.deviceData, deviceData); - assertNotEquals(client.getDeviceData(), deviceData); - } - - @Test - public void testDeviceDataSummary() { - Client client = generateClient(); - DeviceDataSummary deviceData = client.getDeviceDataSummary(); - assertNotNull(deviceData); - assertNotEquals(client.getDeviceDataSummary(), deviceData); + assertEquals(client.getDeviceData(), 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")); + Map metaData = client.getDeviceData().getDeviceMetaData(); + + assertEquals(14, metaData.size()); + assertNotNull(metaData.get("batteryLevel")); + assertNotNull(metaData.get("charging")); + assertNotNull(metaData.get("locationStatus")); + assertNotNull(metaData.get("networkAccess")); + assertNotNull(metaData.get("time")); + assertNotNull(metaData.get("brand")); + assertNotNull(metaData.get("apiLevel")); + assertNotNull(metaData.get("osBuild")); + assertNotNull(metaData.get("locale")); + assertNotNull(metaData.get("screenDensity")); + assertNotNull(metaData.get("dpi")); + assertNotNull(metaData.get("emulator")); + assertNotNull(metaData.get("screenResolution")); + assertNotNull(metaData.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 3f5e4a9611..81df0b5450 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import static com.bugsnag.android.BugsnagTestUtils.generateClient; +import static com.bugsnag.android.BugsnagTestUtils.mapToJson; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -18,17 +19,18 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest public class DeviceDataSummaryTest { - private DeviceDataSummary deviceData; + private Map deviceData; @Before public void setUp() throws Exception { - DeviceDataCollector deviceDataCollector = new DeviceDataCollector(generateClient()); - deviceData = deviceDataCollector.generateDeviceDataSummary(); + DeviceData deviceData = new DeviceData(generateClient()); + this.deviceData = deviceData.getDeviceDataSummary(); } @After @@ -38,39 +40,27 @@ public void tearDown() throws Exception { @Test public void testManufacturer() { - assertNotNull(deviceData.getManufacturer()); - String expected = "Apple"; - deviceData.setManufacturer(expected); // give it 10 years - assertEquals(expected, deviceData.getManufacturer()); + assertNotNull(deviceData.get("manufacturer")); } @Test public void testModel() { - assertNotNull(deviceData.getModel()); - String expected = "Samsung S3"; - deviceData.setModel(expected); - assertEquals(expected, deviceData.getModel()); + assertNotNull(deviceData.get("model")); } @Test public void testOsName() { - assertNotNull(deviceData.getOsName()); - String expected = "ChromeOS"; - deviceData.setOsName(expected); - assertEquals(expected, deviceData.getOsName()); + assertNotNull(deviceData.get("osName")); } @Test public void testOsVersion() { - assertNotNull(deviceData.getOsVersion()); - String expected = "Cyanogen 5"; - deviceData.setOsVersion(expected); - assertEquals(expected, deviceData.getOsVersion()); + assertNotNull(deviceData.get("osVersion")); } @Test - public void testJsonSerialisation() throws IOException, JSONException { - JSONObject deviceDataJson = streamableToJson(deviceData); + public void testJsonSerialisation() throws JSONException { + JSONObject deviceDataJson = mapToJson(deviceData); assertEquals("android", deviceDataJson.getString("osName")); assertNotNull(deviceDataJson.getString("osVersion")); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java index 03004ec2e6..17b54652b9 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java @@ -1,14 +1,12 @@ package com.bugsnag.android; import static com.bugsnag.android.BugsnagTestUtils.generateClient; -import static com.bugsnag.android.BugsnagTestUtils.getSharedPrefs; +import static com.bugsnag.android.BugsnagTestUtils.mapToJson; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import android.content.SharedPreferences; -import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -21,17 +19,18 @@ import java.io.IOException; import java.util.Arrays; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest public class DeviceDataTest { - private DeviceData deviceData; + private Map deviceData; @Before public void setUp() throws Exception { - DeviceDataCollector deviceDataCollector = new DeviceDataCollector(generateClient()); - deviceData = deviceDataCollector.generateDeviceData(); + DeviceData deviceData = new DeviceData(generateClient()); + this.deviceData = deviceData.getDeviceData(); } @After @@ -41,46 +40,27 @@ public void tearDown() throws Exception { @Test public void testId() { - assertNotNull(deviceData.getId()); - String expected = "abc123"; - deviceData.setId(expected); - assertEquals(expected, deviceData.getId()); + assertNotNull(deviceData.get("id")); } @Test public void testOrientation() { - assertNotNull(deviceData.getOrientation()); - String expected = "landscape"; - deviceData.setOrientation(expected); - assertEquals(expected, deviceData.getOrientation()); + assertNotNull(deviceData.get("orientation")); } @Test public void testFreeMemory() { - assertTrue(deviceData.getFreeMemory() > 0); - long expected = 15000000L; - deviceData.setFreeMemory(expected); - assertEquals(expected, deviceData.getFreeMemory()); + assertTrue((Long) deviceData.get("freeMemory") > 0); } @Test public void testTotalMemory() { - assertTrue(deviceData.getTotalMemory() > 0); - long expected = 15000000L; - deviceData.setTotalMemory(expected); - assertEquals(expected, deviceData.getTotalMemory()); - } - - @Test - public void testFreeDisk() { - Long expected = 15000000L; - deviceData.setFreeDisk(expected); - assertEquals(expected, deviceData.getFreeDisk()); + assertTrue((Long) deviceData.get("totalMemory") > 0); } @Test public void testJsonSerialisation() throws IOException, JSONException { - JSONObject deviceDataJson = streamableToJson(deviceData); + JSONObject deviceDataJson = mapToJson(deviceData); // serialises inherited fields correctly for (String key : Arrays.asList("osName", diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java index 1241684914..b58613b5cb 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java @@ -2,7 +2,6 @@ import static com.bugsnag.android.BugsnagTestUtils.generateClient; import static com.bugsnag.android.BugsnagTestUtils.generateSession; -import static com.bugsnag.android.BugsnagTestUtils.generateSessionTracker; import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -12,8 +11,6 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.content.Context; -import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -26,6 +23,7 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest @@ -381,14 +379,13 @@ public void testErrorMetaData() { @Test public void testSetDeviceId() throws Throwable { - DeviceDataCollector deviceDataCollector = new DeviceDataCollector(generateClient()); - DeviceData deviceData = deviceDataCollector.generateDeviceData(); + Map deviceData = new DeviceData(generateClient()).getDeviceData(); error.setDeviceData(deviceData); assertEquals(deviceData, error.getDeviceData()); JSONObject errorJson = streamableToJson(error); JSONObject device = errorJson.getJSONObject("device"); - assertEquals(deviceData.getId(), device.getString("id")); + assertEquals(deviceData.get("id"), device.getString("id")); error.setDeviceId(null); errorJson = streamableToJson(error); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java index 6098feabf9..866e99c810 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java @@ -31,6 +31,7 @@ public class SessionTrackingPayloadTest { private SessionStore sessionStore; private File storageDir; private SessionTrackingPayload payload; + private DeviceData deviceData; /** * Configures a session tracking payload and session store, ensuring that 0 files are present @@ -53,8 +54,10 @@ public void setUp() throws Exception { private SessionTrackingPayload generatePayloadFromSession(Context context, Session session) throws Exception { - appData = generateClient().getAppData(); - return new SessionTrackingPayload(session, appData.getAppDataSummary()); + Client client = generateClient(); + appData = client.getAppData(); + deviceData = client.deviceData; + return new SessionTrackingPayload(session, null, appData, deviceData); } /** @@ -93,7 +96,7 @@ public void testMultipleSessionFiles() throws Exception { sessionStore.write(generateSession()); List storedFiles = sessionStore.findStoredFiles(); - SessionTrackingPayload payload = new SessionTrackingPayload(storedFiles, appData.getAppDataSummary()); + SessionTrackingPayload payload = new SessionTrackingPayload(null, storedFiles, appData, deviceData); rootNode = streamableToJson(payload); assertNotNull(rootNode); diff --git a/sdk/src/main/java/com/bugsnag/android/AppData.java b/sdk/src/main/java/com/bugsnag/android/AppData.java index 210ccf5d08..78a8b3fc0a 100644 --- a/sdk/src/main/java/com/bugsnag/android/AppData.java +++ b/sdk/src/main/java/com/bugsnag/android/AppData.java @@ -61,35 +61,35 @@ class AppData { } Map getAppDataSummary() { - Map map = new ConcurrentHashMap<>(); + Map map = new HashMap<>(); Configuration config = client.config; - putSafely("type", calculateNotifierType(config), map); - putSafely("releaseStage", guessReleaseStage(), map); - putSafely("version", calculateVersionName(), map); - putSafely("versionCode", calculateVersionCode(), map); - putSafely("codeBundleId", config.getCodeBundleId(), map); + map.put("type", calculateNotifierType(config)); + map.put("releaseStage", guessReleaseStage()); + map.put("version", calculateVersionName()); + map.put("versionCode", calculateVersionCode()); + map.put("codeBundleId", config.getCodeBundleId()); return map; } Map getAppData() { Map map = getAppDataSummary(); - putSafely("id", packageName, map); - putSafely("buildUUID", client.config.getBuildUUID(), map); - putSafely("duration", getDurationMs(), map); - putSafely("durationInForeground", calculateDurationInForeground(), map); - putSafely("inForeground", client.sessionTracker.isInForeground(), map); - putSafely("packageName", packageName, map); + map.put("id", packageName); + map.put("buildUUID", client.config.getBuildUUID()); + map.put("duration", getDurationMs()); + map.put("durationInForeground", calculateDurationInForeground()); + map.put("inForeground", client.sessionTracker.isInForeground()); + map.put("packageName", packageName); return map; } Map getAppDataMetaData() { - Map map = new ConcurrentHashMap<>(); - putSafely("name", appName, map); - putSafely("packageName", packageName, map); - putSafely("versionName", calculateVersionName(), map); - putSafely("activeScreen", getActiveScreenClass(), map); - putSafely("memoryUsage", getMemoryUsage(), map); - putSafely("lowMemory", isLowMemory(), map); + Map map = new HashMap<>(); + map.put("name", appName); + map.put("packageName", packageName); + map.put("versionName", calculateVersionName()); + map.put("activeScreen", getActiveScreenClass()); + map.put("memoryUsage", getMemoryUsage()); + map.put("lowMemory", isLowMemory()); return map; } diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index d093b990d9..94f9ca9728 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -69,7 +69,8 @@ public class Client extends Observable implements Observer { @NonNull protected final DeviceData deviceData; - DeviceDataCollector deviceDataCollector; + @NonNull + protected final AppData appData; @NonNull final Breadcrumbs breadcrumbs; @@ -85,7 +86,6 @@ public class Client extends Observable implements Observer { private final EventReceiver eventReceiver; final SessionTracker sessionTracker; SharedPreferences sharedPrefs; - AppData appData; /** * Initialize a Bugsnag client @@ -148,8 +148,7 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat sharedPrefs = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); appData = new AppData(this); - deviceDataCollector = new DeviceDataCollector(this); - deviceData = deviceDataCollector.generateDeviceData(); + deviceData = new DeviceData(this); // Set up breadcrumbs breadcrumbs = new Breadcrumbs(); @@ -159,11 +158,11 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat if (config.getPersistUserBetweenSessions()) { // Check to see if a user was stored in the SharedPreferences - user.setId(sharedPrefs.getString(USER_ID_KEY, deviceData.getId())); + user.setId(sharedPrefs.getString(USER_ID_KEY, getStringFromMap("id", deviceData.getDeviceData()))); user.setName(sharedPrefs.getString(USER_NAME_KEY, null)); user.setEmail(sharedPrefs.getString(USER_EMAIL_KEY, null)); } else { - user.setId(deviceData.getId()); + user.setId(getStringFromMap("id", deviceData.getDeviceData())); } if (appContext instanceof Application) { @@ -570,25 +569,14 @@ public AppData getAppData() { @NonNull @InternalApi public DeviceData getDeviceData() { - return deviceDataCollector.generateDeviceData(); - } - - @NonNull - @InternalApi - public DeviceDataSummary getDeviceDataSummary() { - return deviceDataCollector.generateDeviceDataSummary(); - } - - @InternalApi - public void populateDeviceMetaData(@NonNull MetaData metaData) { - deviceDataCollector.populateDeviceMetaData(metaData); + return deviceData; } /** * Removes the current user data and sets it back to defaults */ public void clearUser() { - user.setId(deviceData.getId()); + user.setId(getStringFromMap("id", deviceData.getDeviceData())); user.setEmail(null); user.setName(null); @@ -937,13 +925,14 @@ void notify(@NonNull Error error, } // Capture the state of the app and device and attach diagnostics to the error - DeviceData errorDeviceData = deviceDataCollector.generateDeviceData(); + Map errorDeviceData = deviceData.getDeviceData(); error.setDeviceData(errorDeviceData); - deviceDataCollector.populateDeviceMetaData(error.getMetaData()); + error.getMetaData().store.put("device", deviceData.getDeviceMetaData()); + // add additional info that belongs in metadata error.setAppData(errorAppData); - error.getMetaData().store.put("app", error.getMetaData()); + error.getMetaData().store.put("app", appData.getAppDataMetaData()); // 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 db4ea75d07..a9631d9b7e 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeviceData.java +++ b/sdk/src/main/java/com/bugsnag/android/DeviceData.java @@ -1,123 +1,411 @@ 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.io.File; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; -/** - * Information about the current Android device which doesn't change over time, - * including screen and locale information. - */ -public class DeviceData extends DeviceDataSummary { +class DeviceData { - private long freeMemory; - private long 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" + }; + + 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 - private Long freeDisk; + Float screenDensity; @Nullable - private String id; + Integer dpi; @Nullable - private String orientation; + String screenResolution; + + @NonNull + String locale; + + @NonNull + String[] cpuAbi; + + DeviceData(Client client) { + this.client = client; + this.appContext = client.appContext; + resources = appContext.getResources(); - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject(); - serialiseMinimalDeviceData(writer); + if (resources != null) { + displayMetrics = resources.getDisplayMetrics(); + } else { + displayMetrics = null; + } + + screenDensity = getScreenDensity(); + dpi = getScreenDensityDpi(); + screenResolution = getScreenResolution(); + locale = getLocale(); + cpuAbi = getCpuAbi(); + emulator = isEmulator(); + id = retrieveUniqueInstallId(); + } + + Map getDeviceDataSummary() { + Map map = new HashMap<>(); + map.put("manufacturer", Build.MANUFACTURER); + map.put("model", Build.MODEL); + map.put("jailbroken", isRooted()); + map.put("osName", "android"); + map.put("osVersion", Build.VERSION.RELEASE); + return map; + } - writer - .name("id").value(id) - .name("freeMemory").value(freeMemory) - .name("totalMemory").value(totalMemory) - .name("freeDisk").value(freeDisk) - .name("orientation").value(orientation); - writer.endObject(); + Map getDeviceData() { + Map map = getDeviceDataSummary(); + map.put("id", id); + map.put("freeMemory", calculateFreeMemory()); + map.put("totalMemory", calculateTotalMemory()); + map.put("freeDisk", calculateFreeDisk()); + map.put("orientation", calculateOrientation()); + return map; + } + + Map getDeviceMetaData() { + Map map = new HashMap<>(); + map.put("batteryLevel", getBatteryLevel()); + map.put("charging", isCharging()); + map.put("locationStatus", getLocationStatus()); + map.put("networkAccess", getNetworkAccess()); + map.put("time", getTime()); + map.put("brand", Build.BRAND); + map.put("apiLevel", Build.VERSION.SDK_INT); + map.put("osBuild", Build.DISPLAY); + map.put("locale", locale); + map.put("screenDensity", screenDensity); + map.put("dpi", dpi); + map.put("emulator", emulator); + map.put("screenResolution", screenResolution); + map.put("cpuAbi", cpuAbi); + return map; } /** - * @return the device's unique ID for the current app installation + * Check if the current Android device is rooted */ - @Nullable - public String getId() { - return id; + 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; } /** - * Overrides the device's unique ID. This can be set to null for privacy reasons, if desired. + * Guesses whether the current device is an emulator or not, erring on the side of caution * - * @param id the new device id + * @return true if the current device is an emulator */ - public void setId(@Nullable String id) { - this.id = id; + private boolean isEmulator() { + String fingerprint = Build.FINGERPRINT; + return fingerprint.startsWith("unknown") + || fingerprint.contains("generic") + || fingerprint.contains("vbox"); // genymotion } /** - * @return the amount of free memory in bytes that the VM can allocate + * The screen density scaling factor of the current Android device */ - public long getFreeMemory() { - return freeMemory; + @Nullable + private Float getScreenDensity() { + if (displayMetrics != null) { + return displayMetrics.density; + } else { + return null; + } } /** - * Overrides the default value for the device's free memory. - * - * @param freeMemory the new free memory value, in bytes + * The screen density of the current Android device in dpi, eg. 320 */ - public void setFreeMemory(long freeMemory) { - this.freeMemory = freeMemory; + @Nullable + private Integer getScreenDensityDpi() { + if (displayMetrics != null) { + return displayMetrics.densityDpi; + } else { + return null; + } } /** - * @return the total amount of memory in bytes that the VM can allocate + * The screen resolution of the current Android device in px, eg. 1920x1080 */ - public long getTotalMemory() { - return totalMemory; + @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; + } } /** - * Overrides the default value for the device's total memory. - * - * @param totalMemory the new total memory value, in bytes + * 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 */ - public void setTotalMemory(long totalMemory) { - this.totalMemory = totalMemory; + @NonNull + private String getLocale() { + return Locale.getDefault().toString(); } /** - * @return the amount of disk space available on the smallest disk on the device, if known + * Get the unique id for the current app installation, creating a unique UUID if needed */ @Nullable - public Long getFreeDisk() { - return freeDisk; + 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; } /** - * Overrides the default value for the device's free disk space, in bytes. - * - * @param freeDisk the new free disk space, in bytes + * 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 */ - public void setFreeDisk(@Nullable Long freeDisk) { - this.freeDisk = freeDisk; + private long calculateFreeMemory() { + Runtime runtime = Runtime.getRuntime(); + if (runtime.maxMemory() != Long.MAX_VALUE) { + return runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory(); + } else { + return runtime.freeMemory(); + } } /** - * @return the device's orientation, if known + * Get the device orientation, eg. "landscape" */ @Nullable - public String getOrientation() { + 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; } /** - * Overrides the device's default orientation - * - * @param orientation the new orientation + * Get the current battery charge level, eg 0.3 */ - public void setOrientation(@Nullable String orientation) { - this.orientation = orientation; + @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/DeviceDataCollector.java b/sdk/src/main/java/com/bugsnag/android/DeviceDataCollector.java deleted file mode 100644 index d9fe7b1075..0000000000 --- a/sdk/src/main/java/com/bugsnag/android/DeviceDataCollector.java +++ /dev/null @@ -1,412 +0,0 @@ -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 deleted file mode 100644 index b0a5dbc2f1..0000000000 --- a/sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.bugsnag.android; - -import android.support.annotation.NonNull; - -import java.io.IOException; - -public class DeviceDataSummary implements JsonStream.Streamable { - - 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; - - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject(); - serialiseMinimalDeviceData(writer); - writer.endObject(); - } - - void serialiseMinimalDeviceData(@NonNull JsonStream writer) throws IOException { - writer - .name("manufacturer").value(manufacturer) - .name("model").value(model) - .name("jailbroken").value(rooted) - .name("osName").value(osName) - .name("osVersion").value(osVersion); - } - - /** - * @return true if the device is rooted, otherwise false - */ - public boolean isJailbroken() { - return rooted; - } - - /** - * Overrides whether the device is rooted or not - * - * @param jailbroken true if the device is rooted - */ - public void setJailbroken(boolean jailbroken) { - this.rooted = jailbroken; - } - - /** - * @return the device manufacturer, determined via {@link android.os.Build#MANUFACTURER} - */ - @NonNull - public String getManufacturer() { - return manufacturer; - } - - /** - * Overrides the device manufacturer - * - * @param manufacturer the new manufacturer - */ - public void setManufacturer(@NonNull String manufacturer) { - this.manufacturer = manufacturer; - } - - /** - * @return the device model, determined via {@link android.os.Build#MODEL} - */ - @NonNull - public String getModel() { - return model; - } - - /** - * Overrides the device model - * - * @param model the new device model - */ - public void setModel(@NonNull String model) { - this.model = model; - } - - /** - * @return the osName, 'Android' by default - */ - @NonNull - public String getOsName() { - return osName; - } - - /** - * Overrides the default osName - * - * @param osName the new osName - */ - public void setOsName(@NonNull String osName) { - this.osName = osName; - } - - /** - * @return the device operating system, determined via {@link android.os.Build.VERSION#RELEASE} - */ - @NonNull - public String getOsVersion() { - return osVersion; - } - - /** - * Overrides the device operating system version - * - * @param osVersion the new os version - */ - public void setOsVersion(@NonNull String osVersion) { - this.osVersion = osVersion; - } - -} diff --git a/sdk/src/main/java/com/bugsnag/android/Error.java b/sdk/src/main/java/com/bugsnag/android/Error.java index 13ea13ac69..7fd37d761f 100644 --- a/sdk/src/main/java/com/bugsnag/android/Error.java +++ b/sdk/src/main/java/com/bugsnag/android/Error.java @@ -24,7 +24,7 @@ public class Error implements JsonStream.Streamable { @SuppressWarnings("NullableProblems") // set after construction @NonNull - private DeviceData deviceData; + private Map deviceData; @SuppressWarnings("NullableProblems") // set after construction @NonNull @@ -344,7 +344,7 @@ public Throwable getException() { * @param id the device id */ public void setDeviceId(@Nullable String id) { - deviceData.setId(id); + deviceData.put("id", id); } /** @@ -363,7 +363,7 @@ Map getAppData() { */ @NonNull - public DeviceData getDeviceData() { + public Map getDeviceData() { return deviceData; } @@ -371,7 +371,7 @@ void setAppData(@NonNull Map appData) { this.appData = appData; } - void setDeviceData(@NonNull DeviceData deviceData) { + void setDeviceData(@NonNull Map deviceData) { this.deviceData = deviceData; } diff --git a/sdk/src/main/java/com/bugsnag/android/MapUtils.java b/sdk/src/main/java/com/bugsnag/android/MapUtils.java index aada556c09..4ab3378be6 100644 --- a/sdk/src/main/java/com/bugsnag/android/MapUtils.java +++ b/sdk/src/main/java/com/bugsnag/android/MapUtils.java @@ -12,10 +12,4 @@ static String getStringFromMap(String key, Map map) { return packageName instanceof String ? (String) packageName : null; } - static void putSafely(String key, @Nullable Object nullableObj, Map map) { - if (nullableObj != null) { - map.put(key, nullableObj); - } - } - } diff --git a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java index 24775cadad..6d3e6ec6c7 100644 --- a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java @@ -118,34 +118,35 @@ public static String getReleaseStage() { @Nullable public static String getDeviceId() { - return getClient().deviceData.getId(); + return getStringFromMap("id", getClient().deviceData.getDeviceData()); } @NonNull public static String getDeviceLocale() { - return getClient().deviceDataCollector.locale; + return getClient().deviceData.locale; } public static double getDeviceTotalMemory() { - return DeviceDataCollector.calculateTotalMemory(); + return DeviceData.calculateTotalMemory(); } - @Nullable public static Boolean getDeviceRooted() { - return getClient().deviceData.isJailbroken(); + Map map = getClient().deviceData.getDeviceDataSummary(); + Object jailbroken = map.get("jailbroken"); + return jailbroken instanceof Boolean ? (Boolean) jailbroken : false; } public static float getDeviceScreenDensity() { - return getClient().deviceDataCollector.screenDensity; + return getClient().deviceData.screenDensity; } public static int getDeviceDpi() { - return getClient().deviceDataCollector.dpi; + return getClient().deviceData.dpi; } @Nullable public static String getDeviceScreenResolution() { - return getClient().deviceDataCollector.screenResolution; + return getClient().deviceData.screenResolution; } public static String getDeviceManufacturer() { @@ -174,7 +175,7 @@ public static int getDeviceApiLevel() { @NonNull public static String[] getDeviceCpuAbi() { - return getClient().deviceDataCollector.cpuAbi; + return getClient().deviceData.cpuAbi; } diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index d46df6d358..a791586ee1 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -94,7 +94,7 @@ public void run() { flushStoredSessions(); SessionTrackingPayload payload = - new SessionTrackingPayload(session, client.appData.getAppDataSummary()); + new SessionTrackingPayload(session, null, client.appData, client.deviceData); try { configuration.getDelivery().deliver(payload, configuration); @@ -159,7 +159,7 @@ void flushStoredSessions() { if (!storedFiles.isEmpty()) { SessionTrackingPayload payload = - new SessionTrackingPayload(storedFiles, client.appData.getAppDataSummary()); + new SessionTrackingPayload(null, storedFiles, client.appData, client.deviceData); //FUTURE:SM Reduce duplication here and above try { diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java b/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java index 1e06854b00..0c08611a8d 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java @@ -13,29 +13,26 @@ public class SessionTrackingPayload implements JsonStream.Streamable { private final Notifier notifier; private final Session session; - private final DeviceDataSummary deviceDataSummary = new DeviceDataSummary(); - private final Map appData; + private final Map deviceDataSummary; + private final Map appDataSummary; private final List files; - SessionTrackingPayload(List files, Map appData) { - this.appData = appData; - this.notifier = Notifier.getInstance(); - this.session = null; - this.files = files; - } - - SessionTrackingPayload(Session session, Map appDataSummary) { - this.appData = appDataSummary; + SessionTrackingPayload(Session session, + List files, + AppData appData, + DeviceData deviceData) { + this.appDataSummary = appData.getAppDataSummary(); + this.deviceDataSummary = deviceData.getDeviceDataSummary(); this.notifier = Notifier.getInstance(); this.session = session; - this.files = null; + this.files = files; } @Override public void toStream(@NonNull JsonStream writer) throws IOException { writer.beginObject(); writer.name("notifier").value(notifier); - writer.name("app").value(appData); + writer.name("app").value(appDataSummary); writer.name("device").value(deviceDataSummary); writer.name("sessions").beginArray(); From d639e58660f334687e809c0fe28d5c81c81e8afe Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Tue, 3 Jul 2018 12:15:40 +0100 Subject: [PATCH 3/4] pass checkstyle --- .../bugsnag/android/SessionTrackingPayloadTest.java | 3 ++- sdk/src/main/java/com/bugsnag/android/AppData.java | 3 --- sdk/src/main/java/com/bugsnag/android/Client.java | 10 ++++++---- .../main/java/com/bugsnag/android/NativeInterface.java | 7 +++++-- .../main/java/com/bugsnag/android/SessionTracker.java | 10 ++++++---- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java index 866e99c810..3ceb2923ff 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java @@ -96,7 +96,8 @@ public void testMultipleSessionFiles() throws Exception { sessionStore.write(generateSession()); List storedFiles = sessionStore.findStoredFiles(); - SessionTrackingPayload payload = new SessionTrackingPayload(null, storedFiles, appData, deviceData); + SessionTrackingPayload payload = new SessionTrackingPayload(null, + storedFiles, appData, deviceData); rootNode = streamableToJson(payload); assertNotNull(rootNode); diff --git a/sdk/src/main/java/com/bugsnag/android/AppData.java b/sdk/src/main/java/com/bugsnag/android/AppData.java index 78a8b3fc0a..196657e1c4 100644 --- a/sdk/src/main/java/com/bugsnag/android/AppData.java +++ b/sdk/src/main/java/com/bugsnag/android/AppData.java @@ -11,9 +11,6 @@ import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import static com.bugsnag.android.MapUtils.*; /** * Collects various data on the application state diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index 94f9ca9728..f34ac9e9e5 100644 --- a/sdk/src/main/java/com/bugsnag/android/Client.java +++ b/sdk/src/main/java/com/bugsnag/android/Client.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import static com.bugsnag.android.MapUtils.getStringFromMap; + import android.app.Activity; import android.app.Application; import android.content.BroadcastReceiver; @@ -25,8 +27,6 @@ import java.util.Observer; import java.util.concurrent.RejectedExecutionException; -import static com.bugsnag.android.MapUtils.getStringFromMap; - /** * A Bugsnag Client instance allows you to use Bugsnag in your Android app. * Typically you'd instead use the static access provided in the Bugsnag class. @@ -156,13 +156,15 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat // Set sensible defaults setProjectPackages(appContext.getPackageName()); + String deviceId = getStringFromMap("id", deviceData.getDeviceData()); + if (config.getPersistUserBetweenSessions()) { // Check to see if a user was stored in the SharedPreferences - user.setId(sharedPrefs.getString(USER_ID_KEY, getStringFromMap("id", deviceData.getDeviceData()))); + user.setId(sharedPrefs.getString(USER_ID_KEY, deviceId)); user.setName(sharedPrefs.getString(USER_NAME_KEY, null)); user.setEmail(sharedPrefs.getString(USER_EMAIL_KEY, null)); } else { - user.setId(getStringFromMap("id", deviceData.getDeviceData())); + user.setId(deviceId); } if (appContext instanceof Application) { diff --git a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java index 6d3e6ec6c7..69b319d307 100644 --- a/sdk/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/sdk/src/main/java/com/bugsnag/android/NativeInterface.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import static com.bugsnag.android.MapUtils.getStringFromMap; + import android.annotation.SuppressLint; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -8,8 +10,6 @@ import java.util.Map; import java.util.Observer; -import static com.bugsnag.android.MapUtils.getStringFromMap; - /** * Used as the entry point for native code to allow proguard to obfuscate other areas if needed */ @@ -130,6 +130,9 @@ public static double getDeviceTotalMemory() { return DeviceData.calculateTotalMemory(); } + /** + * Returns whether a device is rooted or not to the NDK + */ public static Boolean getDeviceRooted() { Map map = getClient().deviceData.getDeviceDataSummary(); Object jailbroken = map.get("jailbroken"); diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index a791586ee1..fd0276e4fa 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import static com.bugsnag.android.MapUtils.getStringFromMap; + import android.app.Activity; import android.app.Application; import android.os.Bundle; @@ -19,8 +21,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import static com.bugsnag.android.MapUtils.getStringFromMap; - class SessionTracker implements Application.ActivityLifecycleCallbacks { private static final String KEY_LIFECYCLE_CALLBACK = "ActivityLifecycle"; @@ -94,7 +94,8 @@ public void run() { flushStoredSessions(); SessionTrackingPayload payload = - new SessionTrackingPayload(session, null, client.appData, client.deviceData); + new SessionTrackingPayload(session, null, + client.appData, client.deviceData); try { configuration.getDelivery().deliver(payload, configuration); @@ -159,7 +160,8 @@ void flushStoredSessions() { if (!storedFiles.isEmpty()) { SessionTrackingPayload payload = - new SessionTrackingPayload(null, storedFiles, client.appData, client.deviceData); + new SessionTrackingPayload(null, storedFiles, + client.appData, client.deviceData); //FUTURE:SM Reduce duplication here and above try { From a11adf068c86d88f1b36b83f586f780299086a09 Mon Sep 17 00:00:00 2001 From: fractalwrench Date: Thu, 5 Jul 2018 14:37:50 +0100 Subject: [PATCH 4/4] assert that the activeScreen attribute is added --- sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java index adad8ba536..7258327596 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -318,6 +318,7 @@ public void testAppDataMetaData() { assertEquals("com.bugsnag.android.test", app.get("packageName")); assertEquals("1.0", app.get("versionName")); assertNotNull(app.get("memoryUsage")); + assertTrue(app.containsKey("activeScreen")); assertNotNull(app.get("lowMemory")); }