diff --git a/.gitignore b/.gitignore index 5d14397a6f..14d9789a32 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ proguard/ # Javadoc docs/ +infer-out/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8086ba8a73..8ece927071 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 4.6.0 (2018-08-02) + +* Android P compatibility fixes - ensure available information on StrictMode violations is collected [#350](https://github.com/bugsnag/bugsnag-android/pull/350) + +* Disable BuildConfig generation [#343](https://github.com/bugsnag/bugsnag-android/pull/343) + +* Add consumer proguard rules for automatic ProGuard configuration without the Bugsnag gradle plugin [#345](https://github.com/bugsnag/bugsnag-android/pull/345) + +* Internal refactor of app/device data serialisation + ## 4.5.0 (2018-06-18) This release alters the behaviour of the notifier to track sessions automatically. diff --git a/README.md b/README.md index 7685661abd..44ef632eca 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build status](https://travis-ci.org/bugsnag/bugsnag-android.svg?branch=master)](https://travis-ci.org/bugsnag/bugsnag-android) [![Coverage Status](https://coveralls.io/repos/github/bugsnag/bugsnag-android/badge.svg?branch=master)](https://coveralls.io/github/bugsnag/bugsnag-android?branch=master) -![Method count and size](https://img.shields.io/badge/Methods%20and%20size-79%20classes%20|%20630%20methods%20|%20312%20fields%20|%20112%20KB-e91e63.svg) +![Method count and size](https://img.shields.io/badge/Methods%20and%20size-78%20classes%20|%20635%20methods%20|%20312%20fields%20|%20112%20KB-e91e63.svg) Get comprehensive [Android crash reports](https://www.bugsnag.com/platforms/android/) to quickly debug errors. diff --git a/build.gradle b/build.gradle index 0918e16927..fc30a9ebf3 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,7 @@ buildscript { google() jcenter() } + ext.kotlin_version = '1.2.30' dependencies { classpath 'com.android.tools.build:gradle:3.0.1' @@ -10,6 +11,7 @@ buildscript { classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0' classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.8.2' classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } repositories { diff --git a/example/build.gradle b/example/build.gradle index fa0b827430..3cf0ec0d0f 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -6,7 +6,6 @@ buildscript { } dependencies { classpath 'com.bugsnag:bugsnag-android-gradle-plugin:3.1.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$project.KOTLIN_VERSION" } } @@ -90,7 +89,7 @@ dependencies { implementation project(":sdk") implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation "com.android.support:support-v4:$supportLibVersion" - kotlinExampleImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$project.KOTLIN_VERSION" + kotlinExampleImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" androidTestImplementation "com.android.support.test:runner:$supportTestVersion", { exclude group: 'com.android.support', module: 'support-annotations' diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 044ece3212..c6142da5e4 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -9,7 +9,7 @@ - + + + + diff --git a/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..036d09bc5f --- /dev/null +++ b/example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..036d09bc5f --- /dev/null +++ b/example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher.png b/example/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000000..6646c295c0 Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/example/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..4faa659c97 Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/example/src/main/res/mipmap-hdpi/ic_launcher_round.png b/example/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..611be91c1e Binary files /dev/null and b/example/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher.png b/example/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000000..5881191e8c Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/example/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..eece0530ff Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/example/src/main/res/mipmap-mdpi/ic_launcher_round.png b/example/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..86f0553f98 Binary files /dev/null and b/example/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000000..2c7092dec9 Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/example/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..4f5b05f781 Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..20b2caf5fd Binary files /dev/null and b/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000000..e8a4d364dd Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/example/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..de4d9e5a17 Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..c69078ac13 Binary files /dev/null and b/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000000..ba2883c126 Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000..05a81fc43f Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..add7c83c40 Binary files /dev/null and b/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/example/src/main/res/values/colors.xml b/example/src/main/res/values/colors.xml index 9a4cef9602..8d460a9aa3 100644 --- a/example/src/main/res/values/colors.xml +++ b/example/src/main/res/values/colors.xml @@ -1,7 +1,7 @@ - #313139 - #212129 - #336ba4 - #071c3b + #010E44 + #20212C + #5BB6CB + #010E44 diff --git a/example/src/main/res/values/ic_launcher_background.xml b/example/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000000..a93ace9fc0 --- /dev/null +++ b/example/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #010E44 + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 0ebe5647dc..2a6d403afb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=4.5.0 +VERSION_NAME=4.6.0 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git diff --git a/infer.sh b/infer.sh new file mode 100755 index 0000000000..c591d7b02e --- /dev/null +++ b/infer.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +./gradlew clean && infer --disable-issue-type "NULL_DEREFERENCE" \ +--disable-issue-type "RESOURCE_LEAK" -- ./gradlew sdk:build diff --git a/mazerunner/build.gradle b/mazerunner/build.gradle index e02c01a1f9..467ab41c20 100644 --- a/mazerunner/build.gradle +++ b/mazerunner/build.gradle @@ -3,16 +3,6 @@ repositories { jcenter() } -buildscript { - repositories { - google() - jcenter() - } - dependencies { - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$project.KOTLIN_VERSION" - } -} - apply plugin: 'com.android.application' apply plugin: 'kotlin-android' diff --git a/sdk/build.gradle b/sdk/build.gradle index ec9af6ea73..866ef599aa 100644 --- a/sdk/build.gradle +++ b/sdk/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'com.getkeepsafe.dexcount' apply plugin: 'maven-publish' apply plugin: 'com.jfrog.bintray' apply plugin: 'com.github.kt3k.coveralls' +apply plugin: 'kotlin-android' repositories { google() @@ -16,6 +17,7 @@ android { defaultConfig { minSdkVersion Integer.parseInt(project.ANDROID_MIN_SDK_VERSION) testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard-rules.pro' } buildTypes { @@ -29,6 +31,17 @@ android { warningsAsErrors true checkAllWarnings true } + + // TODO replace with https://issuetracker.google.com/issues/72050365 once released. + libraryVariants.all { + it.generateBuildConfig.enabled = false + } +} + +gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all" << "-Werror" + } } coveralls { @@ -36,6 +49,9 @@ coveralls { } dependencies { api "com.android.support:support-annotations:$supportLibVersion" + + androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" + testImplementation "junit:junit:$junitVersion" testImplementation "org.mockito:mockito-core:$mockitoVersion" @@ -49,6 +65,7 @@ dependencies { exclude group: 'com.android.support', module: 'support-annotations' } + compile "com.facebook.infer.annotation:infer-annotation:0.11.2" } // Disable doclint: @@ -160,3 +177,4 @@ dexcount { orderByMethodCount = true verbose true } + diff --git a/sdk/proguard-rules.pro b/sdk/proguard-rules.pro index b2abf6f659..793d4ef40f 100644 --- a/sdk/proguard-rules.pro +++ b/sdk/proguard-rules.pro @@ -1,25 +1,7 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in /Users/jamielynch/Library/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile +-keepattributes LineNumberTable,SourceFile +-keep class com.bugsnag.android.NativeInterface { *; } +-keep class com.bugsnag.android.Breadcrumb { *; } +-keep class com.bugsnag.android.Breadcrumbs { *; } +-keep class com.bugsnag.android.BreadcrumbType { *; } +-keep class com.bugsnag.android.Severity { *; } +-keep class com.bugsnag.android.ndk.BugsnagObserver { *; } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java b/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java new file mode 100644 index 0000000000..4b7899cf73 --- /dev/null +++ b/sdk/src/androidTest/java/com/bugsnag/android/AppDataSummaryTest.java @@ -0,0 +1,78 @@ +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; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AppDataSummaryTest { + + private Map appData; + + /** + * Configures a new AppDataSummary for testing accessors + serialisation + * + * @throws Exception if setup failed + */ + @Before + public void setUp() throws Exception { + AppData appData = new AppData(generateClient()); + this.appData = appData.getAppDataSummary(); + } + + @After + public void tearDown() throws Exception { + Async.cancelTasks(); + } + + @Test + public void testVersionCode() { + assertEquals(1, appData.get("versionCode")); + } + + @Test + public void testVersionName() { + assertEquals("1.0", appData.get("version")); + } + + @Test + public void testReleaseStage() { + assertEquals("development", appData.get("releaseStage")); + } + + @Test + public void testNotifierType() { + assertEquals("android", appData.get("type")); + } + + @Test + public void testCodeBundleId() { + assertNull(appData.get("codeBundleId")); + } + + @Test + 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")); + assertEquals("android", appDataJson.get("type")); + } + +} diff --git a/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java index c816786877..7933813096 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/AppDataTest.java @@ -1,12 +1,13 @@ package com.bugsnag.android; -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; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import android.support.annotation.NonNull; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; @@ -19,16 +20,26 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest public class AppDataTest { private Configuration config; - + private Map appData; + private Client client; + + /** + * Configures a new AppData for testing accessors + serialisation + * + * @throws Exception if setup failed + */ @Before public void setUp() throws Exception { config = new Configuration("some-api-key"); + client = new Client(InstrumentationRegistry.getContext(), config); + appData = new AppData(client).getAppData(); } @After @@ -37,48 +48,62 @@ public void tearDown() throws Exception { } @Test - public void testManifestData() throws JSONException, IOException { - AppData appData = generateAppData(); - JSONObject appDataJson = streamableToJson(appData); + public void testPackageName() { + assertEquals("com.bugsnag.android.test", appData.get("packageName")); + } - assertEquals("com.bugsnag.android.test", appDataJson.get("id")); - assertEquals("com.bugsnag.android.test", appDataJson.get("packageName")); - assertEquals("Bugsnag Android Tests", appDataJson.get("name")); - assertEquals(1, appDataJson.get("versionCode")); - assertEquals("1.0", appDataJson.get("versionName")); + @Test + public void testBuildUuid() { + assertNull(appData.get("buildUUID")); + } + + @Test + public void testDuration() { + assertTrue(((Long) appData.get("duration")) > 0); + } + + @Test + public void testDurationInForeground() { + assertEquals(0L, appData.get("durationInForeground")); + } + + @Test + public void testInForeground() { + assertFalse((Boolean) appData.get("inForeground")); + } + + @Test + public void testJsonSerialisation() throws JSONException { + appData.put("buildUUID", "fa54de"); + JSONObject appDataJson = mapToJson(appData); + + assertEquals(1, appDataJson.getInt("versionCode")); assertEquals("1.0", appDataJson.get("version")); assertEquals("development", appDataJson.get("releaseStage")); - - assertTrue(appDataJson.getLong("memoryUsage") > 0); - assertNotNull(appDataJson.getBoolean("lowMemory")); - assertTrue(appDataJson.getLong("duration") >= 0); + assertEquals("android", appDataJson.get("type")); + assertEquals("com.bugsnag.android.test", appDataJson.get("id")); + assertNotNull(appDataJson.get("buildUUID")); + assertNotNull(appDataJson.get("duration")); + assertNotNull(appDataJson.get("durationInForeground")); + assertFalse(appDataJson.getBoolean("inForeground")); } @Test - public void testAppVersionOverride() throws JSONException, IOException { + public void testAppVersionOverride() throws JSONException { String appVersion = "1.2.3"; config.setAppVersion(appVersion); - AppData appData = 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 appData = generateAppData(); - JSONObject appDataJson = streamableToJson(appData); - + JSONObject appDataJson = mapToJson(client.appData.getAppData()); assertEquals(releaseStage, appDataJson.get("releaseStage")); } - @NonNull - private AppData generateAppData() { - return new AppData(InstrumentationRegistry.getContext(), config, generateSessionTracker()); - } - } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/BeforeNotifyTest.java b/sdk/src/androidTest/java/com/bugsnag/android/BeforeNotifyTest.java deleted file mode 100644 index 80f38b8787..0000000000 --- a/sdk/src/androidTest/java/com/bugsnag/android/BeforeNotifyTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.bugsnag.android; - -import static org.junit.Assert.assertEquals; - -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class BeforeNotifyTest { - - private BeforeNotify beforeNotify = new BeforeNotify() { - @Override - public boolean run(Error error) { - return true; - } - }; - - private BeforeNotify beforeNotifySkip = new BeforeNotify() { - @Override - public boolean run(Error error) { - return false; - } - }; - - private Configuration config; - - @Before - public void setUp() throws Exception { - config = new Configuration("api-key"); - } - - @After - public void tearDown() throws Exception { - Async.cancelTasks(); - } - - @Test - public void testRunModifiesError() { - final String context = "new-context"; - - BeforeNotify beforeNotify = new BeforeNotify() { - @Override - public boolean run(Error error) { - error.setContext(context); - return false; - } - }; - - Error error = new Error.Builder(config, new RuntimeException("Test"), null).build(); - beforeNotify.run(error); - - assertEquals(context, error.getContext()); - } -} diff --git a/sdk/src/androidTest/java/com/bugsnag/android/BeforeNotifyTest.kt b/sdk/src/androidTest/java/com/bugsnag/android/BeforeNotifyTest.kt new file mode 100644 index 0000000000..2a2a82c646 --- /dev/null +++ b/sdk/src/androidTest/java/com/bugsnag/android/BeforeNotifyTest.kt @@ -0,0 +1,30 @@ +package com.bugsnag.android + +import org.junit.Assert.assertEquals + +import android.support.test.filters.SmallTest +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@SmallTest +class BeforeNotifyTest { + + private val config = Configuration("api-key") + + @Test + fun testRunModifiesError() { + val context = "new-context" + + val beforeNotify = BeforeNotify { + it.context = context + false + } + + val error = Error.Builder(config, RuntimeException("Test"), null).build() + beforeNotify.run(error) + assertEquals(context, error.context) + } +} diff --git a/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java b/sdk/src/androidTest/java/com/bugsnag/android/BugsnagTestUtils.java index 59eaaa1356..fa9f8612cb 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)); @@ -66,6 +70,7 @@ static SessionStore generateSessionStore() { return new SessionStore(generateConfiguration(), InstrumentationRegistry.getContext()); } + @SuppressWarnings("deprecation") @NonNull static SessionTrackingApiClient generateSessionTrackingApiClient() { return new SessionTrackingApiClient() { @@ -79,6 +84,7 @@ public void postSessionTrackingPayload(String urlString, }; } + @SuppressWarnings("deprecation") static ErrorReportApiClient generateErrorReportApiClient() { // no-op return new ErrorReportApiClient() { @Override diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java index 3c42e1b44c..db8692112d 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientConfigTest.java @@ -63,6 +63,7 @@ public void testSetContext() throws Exception { assertEquals("JunitTest", config.getContext()); } + @SuppressWarnings("deprecation") @Test public void testSetEndpoint() throws Exception { client.setEndpoint("http://example.com/bugsnag"); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientNotifyTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientNotifyTest.java index 127f6c880a..ad063b444c 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientNotifyTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientNotifyTest.java @@ -26,6 +26,7 @@ public class ClientNotifyTest { * * @throws Exception if initialisation failed */ + @SuppressWarnings("deprecation") @Before public void setUp() throws Exception { client = BugsnagTestUtils.generateClient(); @@ -81,6 +82,7 @@ public void beforeNotify(Report report) { assertEquals("Message", error.getExceptionMessage()); } + @SuppressWarnings("deprecation") static class FakeClient implements ErrorReportApiClient { CountDownLatch latch = new CountDownLatch(1); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java index 87a932d1b5..29a0e16d5c 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ClientTest.java @@ -2,13 +2,16 @@ import static com.bugsnag.android.BugsnagTestUtils.generateClient; import static com.bugsnag.android.BugsnagTestUtils.getSharedPrefs; +import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -19,6 +22,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Collection; import java.util.Map; @RunWith(AndroidJUnit4.class) @@ -86,6 +90,7 @@ public void testNotify() { client.notify(new RuntimeException("Testing")); } + @SuppressWarnings("deprecation") @Test public void testConfig() { config.setEndpoint("new-endpoint"); @@ -103,9 +108,8 @@ public void testRestoreUserFromPrefs() { setUserPrefs(); config.setPersistUserBetweenSessions(true); + config.setDelivery(BugsnagTestUtils.generateDelivery()); Client client = new Client(context, config); - client.setErrorReportApiClient(BugsnagTestUtils.generateErrorReportApiClient()); - client.setSessionTrackingApiClient(BugsnagTestUtils.generateSessionTrackingApiClient()); final User user = new User(); @@ -265,9 +269,86 @@ public void testClientClearTab() { assertTrue(client.getMetaData().getTab("drink").isEmpty()); } + @SuppressWarnings("deprecation") @Test(expected = IllegalArgumentException.class) public void testApiClientNullValidation() { generateClient().setSessionTrackingApiClient(null); } + @Test + public void testClientUser() { + Client client = generateClient(); + assertNotNull(client.getUser()); + assertNotNull(client.getUser().getId()); + } + + @Test + public void testBreadcrumbGetter() { + Client client = generateClient(); + Collection breadcrumbs = client.getBreadcrumbs(); + + int breadcrumbCount = breadcrumbs.size(); + client.leaveBreadcrumb("Foo"); + assertEquals(breadcrumbCount, breadcrumbs.size()); // should not pick up new breadcrumbs + } + + @Test + public void testBreadcrumbStoreNotModified() { + Client client = generateClient(); + Collection breadcrumbs = client.getBreadcrumbs(); + int breadcrumbCount = client.breadcrumbs.store.size(); + + breadcrumbs.clear(); // only the copy should be cleared + assertTrue(breadcrumbs.isEmpty()); + assertEquals(breadcrumbCount, client.breadcrumbs.store.size()); + } + + @Test + public void testAppDataCollection() { + Client client = generateClient(); + AppData appData = client.getAppData(); + assertEquals(client.getAppData(), appData); + } + + @Test + public void testAppDataMetaData() { + Client client = generateClient(); + Map app = client.getAppData().getAppDataMetaData(); + 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")); + assertNotNull(app.get("memoryUsage")); + assertTrue(app.containsKey("activeScreen")); + assertNotNull(app.get("lowMemory")); + } + + @Test + public void testDeviceDataCollection() { + Client client = generateClient(); + DeviceData deviceData = client.getDeviceData(); + assertEquals(client.getDeviceData(), deviceData); + } + + @Test + public void testPopulateDeviceMetadata() { + Client client = generateClient(); + 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/ConfigurationTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ConfigurationTest.java index 20d3c39951..b989c8a009 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ConfigurationTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ConfigurationTest.java @@ -77,6 +77,7 @@ public void testAutoCaptureOverride() { assertFalse(config.shouldAutoCaptureSessions()); } + @SuppressWarnings("deprecation") @Test public void testEndpoint() { // Default endpoints @@ -88,6 +89,7 @@ public void testEndpoint() { assertEquals(endpoint, config.getEndpoint()); } + @SuppressWarnings("deprecation") @Test public void testSessionEndpoint() { // Default endpoints diff --git a/sdk/src/androidTest/java/com/bugsnag/android/CustomException.java b/sdk/src/androidTest/java/com/bugsnag/android/CustomException.java index 1920f37206..bfb0767286 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/CustomException.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/CustomException.java @@ -6,6 +6,8 @@ class CustomException extends Exception implements JsonStream.Streamable { + private static final long serialVersionUID = 1962801050513549513L; + CustomException(String message) { super(message); } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/DeliveryCompatTest.java b/sdk/src/androidTest/java/com/bugsnag/android/DeliveryCompatTest.java index 1d5628b0fd..3ecf23ab58 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/DeliveryCompatTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeliveryCompatTest.java @@ -35,6 +35,7 @@ public void tearDown() throws Exception { Async.cancelTasks(); } + @SuppressWarnings("deprecation") @Test public void deliverReport() throws Exception { Report report = null; @@ -54,6 +55,7 @@ public void postReport(String urlString, assertEquals(1, customCount.get()); } + @SuppressWarnings("deprecation") @Test public void deliverSession() throws Exception { SessionTrackingPayload payload = null; @@ -73,6 +75,7 @@ public void postSessionTrackingPayload(String urlString, assertEquals(1, customCount.get()); } + @SuppressWarnings("deprecation") @Test public void testClientCompat() { Client client = BugsnagTestUtils.generateClient(); @@ -96,11 +99,13 @@ public void testClientCompat() { assertEquals(sessionClient, compat.sessionTrackingApiClient); } + @SuppressWarnings("deprecation") @Test(expected = DeliveryFailureException.class) public void testExceptionConversion() throws Exception { deliveryCompat.handleException(new NetworkException("", null)); } + @SuppressWarnings("deprecation") @Test public void testSwallowExceptionConversion() throws Exception { // no exception thrown deliveryCompat.handleException(new BadResponseException("", 0)); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java new file mode 100644 index 0000000000..81df0b5450 --- /dev/null +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataSummaryTest.java @@ -0,0 +1,72 @@ +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; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DeviceDataSummaryTest { + + private Map deviceData; + + @Before + public void setUp() throws Exception { + DeviceData deviceData = new DeviceData(generateClient()); + this.deviceData = deviceData.getDeviceDataSummary(); + } + + @After + public void tearDown() throws Exception { + Async.cancelTasks(); + } + + @Test + public void testManufacturer() { + assertNotNull(deviceData.get("manufacturer")); + } + + @Test + public void testModel() { + assertNotNull(deviceData.get("model")); + } + + @Test + public void testOsName() { + assertNotNull(deviceData.get("osName")); + } + + @Test + public void testOsVersion() { + assertNotNull(deviceData.get("osVersion")); + } + + @Test + public void testJsonSerialisation() throws JSONException { + JSONObject deviceDataJson = mapToJson(deviceData); + + assertEquals("android", deviceDataJson.getString("osName")); + assertNotNull(deviceDataJson.getString("osVersion")); + assertNotNull(deviceDataJson.getString("manufacturer")); + assertNotNull(deviceDataJson.getString("model")); + assertTrue(deviceDataJson.has("jailbroken")); + } + +} diff --git a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java index 040c06df7d..17b54652b9 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/DeviceDataTest.java @@ -1,20 +1,14 @@ package com.bugsnag.android; -import static com.bugsnag.android.BugsnagTestUtils.getSharedPrefs; +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.assertNotNull; -import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.SharedPreferences; -import android.provider.Settings; -import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import android.util.DisplayMetrics; import org.json.JSONException; import org.json.JSONObject; @@ -24,17 +18,19 @@ import org.junit.runner.RunWith; 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 { - SharedPreferences sharedPref = getSharedPrefs(InstrumentationRegistry.getContext()); - deviceData = new DeviceData(InstrumentationRegistry.getContext(), sharedPref); + DeviceData deviceData = new DeviceData(generateClient()); + this.deviceData = deviceData.getDeviceData(); } @After @@ -43,39 +39,40 @@ public void tearDown() throws Exception { } @Test - public void testSaneValues() throws JSONException, IOException { - JSONObject deviceDataJson = streamableToJson(deviceData); - - assertEquals("android", deviceDataJson.getString("osName")); - assertTrue(deviceDataJson.getString("manufacturer").length() > 1); - assertTrue(deviceDataJson.getString("brand").length() > 1); - assertTrue(deviceDataJson.getString("model").length() > 1); - - assertTrue(deviceDataJson.getDouble("screenDensity") > 0); - assertTrue(deviceDataJson.getDouble("dpi") >= DisplayMetrics.DENSITY_LOW); - String screenResolution = deviceDataJson.getString("screenResolution"); - assertTrue(screenResolution.matches("^\\d+x\\d+$")); - assertTrue(deviceDataJson.getLong("totalMemory") > 0); - assertNotNull(deviceDataJson.getBoolean("jailbroken")); - assertNotNull(deviceDataJson.getString("locale")); - assertNotNull(deviceDataJson.getString("cpuAbi")); + public void testId() { + assertNotNull(deviceData.get("id")); + } - // Emulators returned null for android id before android 2.2 - assertNotNull(deviceDataJson.getString("id")); + @Test + public void testOrientation() { + assertNotNull(deviceData.get("orientation")); + } - // historically Android ID was used, this should no longer be the case - ContentResolver cr = InstrumentationRegistry.getContext().getContentResolver(); - @SuppressLint("HardwareIds") - String androidId = Settings.Secure.getString(cr, Settings.Secure.ANDROID_ID); - assertNotSame(androidId, deviceDataJson.getString("id")); + @Test + public void testFreeMemory() { + assertTrue((Long) deviceData.get("freeMemory") > 0); + } + @Test + public void testTotalMemory() { + assertTrue((Long) deviceData.get("totalMemory") > 0); + } + + @Test + public void testJsonSerialisation() throws IOException, JSONException { + JSONObject deviceDataJson = mapToJson(deviceData); + + // serialises inherited fields correctly + for (String key : Arrays.asList("osName", + "osVersion", "manufacturer", "model", "jailbroken")) { + assertTrue(deviceDataJson.has(key)); + } + + assertNotNull(deviceDataJson.getString("id")); assertTrue(deviceDataJson.getLong("freeMemory") > 0); - assertNotNull(deviceDataJson.get("orientation")); - assertTrue(deviceDataJson.getDouble("batteryLevel") > 0); - assertTrue(deviceDataJson.getBoolean("charging")); - assertEquals("allowed", deviceDataJson.getString("locationStatus")); - assertNotNull(deviceDataJson.get("networkAccess")); - assertNotNull(deviceDataJson.get("time")); + assertTrue(deviceDataJson.getLong("totalMemory") > 0); + assertTrue(deviceDataJson.has("freeDisk")); + assertNotNull(deviceDataJson.getString("orientation")); } } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ErrorReportApiClientTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ErrorReportApiClientTest.java index 42465567dd..470a20e44b 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ErrorReportApiClientTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ErrorReportApiClientTest.java @@ -31,11 +31,13 @@ public void tearDown() throws Exception { Async.cancelTasks(); } + @SuppressWarnings("deprecation") @Test(expected = IllegalArgumentException.class) public void testApiClientNullValidation() { Bugsnag.setErrorReportApiClient(null); } + @SuppressWarnings("deprecation") @Test public void testPostReportCalled() { Bugsnag.setErrorReportApiClient(apiClient); @@ -46,6 +48,7 @@ public void testPostReportCalled() { assertNotNull(apiClient.report); } + @SuppressWarnings("deprecation") private static class FakeApiClient implements ErrorReportApiClient { private Report report; diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java index fb2a1da0bd..b58613b5cb 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ErrorTest.java @@ -1,7 +1,7 @@ package com.bugsnag.android; +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; @@ -11,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; @@ -25,6 +23,7 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.Map; @RunWith(AndroidJUnit4.class) @SmallTest @@ -77,11 +76,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 @@ -222,6 +227,7 @@ public void testSetContext() throws JSONException, IOException { public void testSetGroupingHash() throws JSONException, IOException { String groupingHash = "herpderp"; error.setGroupingHash(groupingHash); + assertEquals(groupingHash, error.getGroupingHash()); JSONObject errorJson = streamableToJson(error); assertEquals(groupingHash, errorJson.get("groupingHash")); @@ -312,15 +318,10 @@ public void testNullContext() throws Exception { } @Test - public void testAppDataContext() throws Exception { + public void testActiveScreen() throws Exception { error.setContext(null); - Context context = InstrumentationRegistry.getContext(); - SessionTracker sessionTracker = generateSessionTracker(); - String expectedContext = "FooActivity"; - sessionTracker.updateForegroundTracker(expectedContext, - true, System.currentTimeMillis()); - error.setAppData(new AppData(context, config, sessionTracker)); - assertEquals(expectedContext, error.getContext()); + error.getMetaData().addToTab("app", "activeScreen", "FooActivity"); + assertEquals("FooActivity", error.getContext()); } @Test @@ -378,13 +379,13 @@ public void testErrorMetaData() { @Test public void testSetDeviceId() throws Throwable { - Context context = InstrumentationRegistry.getContext(); - DeviceData deviceData = new DeviceData(context, BugsnagTestUtils.getSharedPrefs(context)); + Map deviceData = new DeviceData(generateClient()).getDeviceData(); error.setDeviceData(deviceData); + assertEquals(deviceData, error.getDeviceData()); JSONObject errorJson = streamableToJson(error); JSONObject device = errorJson.getJSONObject("device"); - assertEquals(deviceData.id, 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/MetaDataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/MetaDataTest.java index 23b5eab465..75497dc6d5 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/MetaDataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/MetaDataTest.java @@ -167,6 +167,8 @@ public void testDeepMerge() { MetaData merged = MetaData.merge(base, overrides); Map tab = merged.getTab("example"); + + @SuppressWarnings("unchecked") Map mergedMap = (Map) tab.get("map"); assertEquals("fromOverrides", mergedMap.get("key")); } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/NotifierTest.java b/sdk/src/androidTest/java/com/bugsnag/android/NotifierTest.java new file mode 100644 index 0000000000..09c32e7e58 --- /dev/null +++ b/sdk/src/androidTest/java/com/bugsnag/android/NotifierTest.java @@ -0,0 +1,61 @@ +package com.bugsnag.android; + +import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class NotifierTest { + + private Notifier notifier; + + @Before + public void setUp() throws Exception { + notifier = new Notifier(); + } + + @Test + public void testName() { + assertEquals("Android Bugsnag Notifier", notifier.getName()); + String expected = "CrossPlatformFramework"; + notifier.setName(expected); + assertEquals(expected, notifier.getName()); + } + + @Test + public void testVersion() { + assertNotNull(notifier.getVersion()); + String expected = "1.2.3"; + notifier.setVersion(expected); + assertEquals(expected, notifier.getVersion()); + } + + @Test + public void testUrl() { + assertEquals("https://bugsnag.com", notifier.getURL()); + String expected = "http://example.com"; + notifier.setURL(expected); + assertEquals(expected, notifier.getURL()); + } + + @Test + public void testJsonSerialisation() throws JSONException, IOException { + JSONObject notifierJson = streamableToJson(notifier); + assertEquals(3, notifierJson.length()); + assertEquals("Android Bugsnag Notifier", notifierJson.getString("name")); + assertNotNull(notifierJson.getString("version")); + assertEquals("https://bugsnag.com", notifierJson.getString("url")); + } +} diff --git a/sdk/src/androidTest/java/com/bugsnag/android/NullMetadataTest.java b/sdk/src/androidTest/java/com/bugsnag/android/NullMetadataTest.java index aaca1320b7..dbe7c969c5 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/NullMetadataTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/NullMetadataTest.java @@ -31,17 +31,7 @@ public class NullMetadataTest { @Before public void setUp() throws Exception { config = new Configuration("api-key"); - Bugsnag.init(InstrumentationRegistry.getContext(), config); - Bugsnag.setErrorReportApiClient(new ErrorReportApiClient() { - @Override - public void postReport(String urlString, - Report report, - Map headers) - throws NetworkException, BadResponseException { - - } - }); - + BugsnagTestUtils.generateClient(); throwable = new RuntimeException("Test"); } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/ReportTest.java b/sdk/src/androidTest/java/com/bugsnag/android/ReportTest.java index 42ff605d69..1ec84e37bd 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/ReportTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/ReportTest.java @@ -57,6 +57,7 @@ public void testModifyingGroupingHash() throws JSONException, IOException { assertEquals(groupingHash, event.getString("groupingHash")); } + @SuppressWarnings("deprecation") @Test public void testModifyReportDetails() throws Exception { String apiKey = "custom-api-key"; diff --git a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java index b7f83577f8..3ceb2923ff 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/SessionTrackingPayloadTest.java @@ -1,7 +1,7 @@ package com.bugsnag.android; +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; @@ -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 = new AppData(context, new Configuration("a"), generateSessionTracker()); - return new SessionTrackingPayload(session, appData); + Client client = generateClient(); + appData = client.getAppData(); + deviceData = client.deviceData; + return new SessionTrackingPayload(session, null, appData, deviceData); } /** @@ -93,7 +96,8 @@ public void testMultipleSessionFiles() throws Exception { sessionStore.write(generateSession()); List storedFiles = sessionStore.findStoredFiles(); - SessionTrackingPayload payload = new SessionTrackingPayload(storedFiles, appData); + SessionTrackingPayload payload = new SessionTrackingPayload(null, + storedFiles, appData, deviceData); rootNode = streamableToJson(payload); assertNotNull(rootNode); diff --git a/sdk/src/androidTest/java/com/bugsnag/android/StrictModeTest.java b/sdk/src/androidTest/java/com/bugsnag/android/StrictModeTest.java index edc0385b47..d219603f86 100644 --- a/sdk/src/androidTest/java/com/bugsnag/android/StrictModeTest.java +++ b/sdk/src/androidTest/java/com/bugsnag/android/StrictModeTest.java @@ -7,6 +7,7 @@ import static junit.framework.Assert.fail; import android.content.Context; +import android.os.Build; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; @@ -97,7 +98,14 @@ public void testStrictModeDescException() { if (exception != null) { String desc = strictModeHandler.getViolationDescription(exception.getMessage()); - assertEquals("DiskRead", desc); + + if (Build.VERSION.SDK_INT >= 28) { + // the violation description format changed to be more generic in P, + // no longer possible to get a full description + assertNull(desc); + } else { + assertEquals("DiskRead", desc); + } } } diff --git a/sdk/src/androidTest/java/com/bugsnag/android/UserTest.java b/sdk/src/androidTest/java/com/bugsnag/android/UserTest.java new file mode 100644 index 0000000000..044896f814 --- /dev/null +++ b/sdk/src/androidTest/java/com/bugsnag/android/UserTest.java @@ -0,0 +1,62 @@ +package com.bugsnag.android; + +import static com.bugsnag.android.BugsnagTestUtils.streamableToJson; +import static org.junit.Assert.assertEquals; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UserTest { + + private User user; + + @Before + public void setUp() throws Exception { + user = new User("123", "bob@example.com", "bob smith"); + } + + @Test + public void testUserDefaults() { + assertEquals("123", user.getId()); + assertEquals("bob smith", user.getName()); + assertEquals("bob@example.com", user.getEmail()); + } + + @Test + public void testUserCopy() { + user = new User(user); + assertEquals("123", user.getId()); + assertEquals("bob smith", user.getName()); + assertEquals("bob@example.com", user.getEmail()); + } + + @Test + public void testUserOverride() { + user.setId("4fd"); + user.setName("jane"); + user.setEmail("jane@example.com"); + assertEquals("4fd", user.getId()); + assertEquals("jane", user.getName()); + assertEquals("jane@example.com", user.getEmail()); + } + + @Test + public void testJsonSerialisation() throws JSONException, IOException { + JSONObject userJson = streamableToJson(user); + assertEquals(3, userJson.length()); + assertEquals("123", userJson.get("id")); + assertEquals("bob smith", userJson.get("name")); + assertEquals("bob@example.com", userJson.get("email")); + } + +} diff --git a/sdk/src/main/java/com/bugsnag/android/AppData.java b/sdk/src/main/java/com/bugsnag/android/AppData.java index 55a83e2a7a..196657e1c4 100644 --- a/sdk/src/main/java/com/bugsnag/android/AppData.java +++ b/sdk/src/main/java/com/bugsnag/android/AppData.java @@ -3,99 +3,203 @@ 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; /** - * Information about the running Android app, including app name, version and release stage. + * Collects various data on the application state */ -class AppData extends AppDataSummary { +class AppData { 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; - @NonNull - private final Context appContext; - private final SessionTracker sessionTracker; + @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(); + } + + Map getAppDataSummary() { + Map map = new HashMap<>(); + Configuration config = client.config; + 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(); + 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 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; + } + + /** + * 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 - protected final String packageName; - - AppData(@NonNull Context appContext, - @NonNull Configuration config, - SessionTracker sessionTracker) { - super(appContext, config); - this.appContext = appContext; - this.sessionTracker = sessionTracker; - appName = getAppName(appContext); - packageName = getPackageName(appContext); + private String calculateNotifierType(Configuration config) { + String notifierType = config.getNotifierType(); + + if (notifierType != null) { + return notifierType; + } else { + return "android"; + } } - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject(); - serialiseMinimalAppData(writer); - - writer.name("id").value(packageName); - writer.name("buildUUID").value(config.getBuildUUID()); - writer.name("duration").value(getDurationMs()); - long foregroundMs = sessionTracker.getDurationInForegroundMs(System.currentTimeMillis()); - writer.name("durationInForeground").value(foregroundMs); - writer.name("inForeground").value(sessionTracker.isInForeground()); - - // TODO migrate legacy fields - writer.name("name").value(appName); - writer.name("packageName").value(packageName); - writer.name("versionName").value(versionName); - writer.name("activeScreen").value(getActiveScreenClass()); - writer.name("memoryUsage").value(getMemoryUsage()); - writer.name("lowMemory").value(isLowMemory(appContext)); - writer.endObject(); + /** + * 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 name of the running Android app, from android:label in - * AndroidManifest.xml + * The version code of the running Android app, from android:versionName + * in AndroidManifest.xml */ @Nullable - private static String getAppName(@NonNull Context appContext) { - try { - PackageManager packageManager = appContext.getPackageManager(); - String packageName = appContext.getPackageName(); - ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); + private String calculateVersionName() { + String configAppVersion = client.config.getAppVersion(); + + if (configAppVersion != null) { + return configAppVersion; + } else if (packageInfo != null) { + return packageInfo.versionName; + } else { + return null; + } + } - return (String) packageManager.getApplicationLabel(appInfo); - } catch (PackageManager.NameNotFoundException exception) { - Logger.warn("Could not get app name"); + /** + * 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; } - return null; + 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 - String getActiveScreenClass() { - return sessionTracker.getContextActivity(); + 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). */ - @NonNull - private static Long getMemoryUsage() { - return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + private long getMemoryUsage() { + Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); } /** * Check if the device is currently running low on memory. */ @Nullable - private static Boolean isLowMemory(@NonNull Context appContext) { + private Boolean isLowMemory() { try { ActivityManager activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); @@ -111,20 +215,4 @@ private static Boolean isLowMemory(@NonNull Context appContext) { return null; } - /** - * 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; - } - - - /** - * The package name of the running Android app, eg: com.example.myapp - */ - @NonNull - private static String getPackageName(@NonNull Context appContext) { - return appContext.getPackageName(); - } } 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 fd591b65aa..0000000000 --- a/sdk/src/main/java/com/bugsnag/android/AppDataSummary.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.bugsnag.android; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -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. - */ -class AppDataSummary implements JsonStream.Streamable { - - static final String RELEASE_STAGE_DEVELOPMENT = "development"; - static final String RELEASE_STAGE_PRODUCTION = "production"; - - @NonNull - protected final Configuration config; - - @Nullable - protected final Integer versionCode; - - @Nullable - protected final String versionName; - - @NonNull - private final String guessedReleaseStage; - - @Nullable - private String notifierType = "android"; - - @Nullable - private String codeBundleId; - - AppDataSummary(@NonNull Context appContext, @NonNull Configuration config) { - versionCode = getVersionCode(appContext); - versionName = getVersionName(appContext); - guessedReleaseStage = guessReleaseStage(appContext); - this.config = config; - - codeBundleId = config.getCodeBundleId(); - String configType = config.getNotifierType(); - - if (configType != null) { - notifierType = configType; - } - } - - @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(getReleaseStage()) - .name("version").value(getAppVersion()) - .name("versionCode").value(versionCode) - .name("codeBundleId").value(codeBundleId); - } - - @NonNull - String getReleaseStage() { - if (config.getReleaseStage() != null) { - return config.getReleaseStage(); - } else { - return guessedReleaseStage; - } - } - - @Nullable - String getAppVersion() { - if (config.getAppVersion() != null) { - return config.getAppVersion(); - } else { - return versionName; - } - } - - /** - * The version code of the running Android app, from android:versionCode - * in AndroidManifest.xml - */ - @Nullable - private static Integer getVersionCode(@NonNull Context appContext) { - try { - String packageName = appContext.getPackageName(); - return appContext.getPackageManager().getPackageInfo(packageName, 0).versionCode; - } catch (PackageManager.NameNotFoundException exception) { - Logger.warn("Could not get versionCode"); - } - return null; - } - - /** - * The version code of the running Android app, from android:versionName - * in AndroidManifest.xml - */ - @Nullable - private static String getVersionName(@NonNull Context appContext) { - try { - String packageName = appContext.getPackageName(); - return appContext.getPackageManager().getPackageInfo(packageName, 0).versionName; - } catch (PackageManager.NameNotFoundException exception) { - Logger.warn("Could not get versionName"); - } - return null; - } - - /** - * Guess the release stage of the running Android app by checking the - * android:debuggable flag from AndroidManifest.xml - */ - @NonNull - static String guessReleaseStage(@NonNull Context appContext) { - try { - String packageName = appContext.getPackageName(); - PackageManager packageManager = appContext.getPackageManager(); - int appFlags = packageManager.getApplicationInfo(packageName, 0).flags; - if ((appFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { - return RELEASE_STAGE_DEVELOPMENT; - } - } catch (PackageManager.NameNotFoundException exception) { - Logger.warn("Could not get releaseStage"); - } - return RELEASE_STAGE_PRODUCTION; - } - -} diff --git a/sdk/src/main/java/com/bugsnag/android/Async.java b/sdk/src/main/java/com/bugsnag/android/Async.java index 3993b7afca..fdc334df0b 100644 --- a/sdk/src/main/java/com/bugsnag/android/Async.java +++ b/sdk/src/main/java/com/bugsnag/android/Async.java @@ -3,7 +3,6 @@ import android.support.annotation.NonNull; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadFactory; diff --git a/sdk/src/main/java/com/bugsnag/android/BadResponseException.java b/sdk/src/main/java/com/bugsnag/android/BadResponseException.java index db0fad2378..e3514a1d9f 100644 --- a/sdk/src/main/java/com/bugsnag/android/BadResponseException.java +++ b/sdk/src/main/java/com/bugsnag/android/BadResponseException.java @@ -1,12 +1,17 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + import java.util.Locale; /** * @deprecated use {@link DeliveryFailureException} instead */ @Deprecated +@ThreadSafe public class BadResponseException extends Exception { + private static final long serialVersionUID = -870190454845379171L; + public BadResponseException(String msg, int responseCode) { super(String.format(Locale.US, "%s (%d)", msg, responseCode)); } diff --git a/sdk/src/main/java/com/bugsnag/android/BeforeNotify.java b/sdk/src/main/java/com/bugsnag/android/BeforeNotify.java index 818c3c3567..dfb62bb532 100644 --- a/sdk/src/main/java/com/bugsnag/android/BeforeNotify.java +++ b/sdk/src/main/java/com/bugsnag/android/BeforeNotify.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + /** * A callback to be run before every report to Bugsnag. *

@@ -7,6 +9,7 @@ * before it is sent to your dashboard. You can also return * false from any callback to halt execution. */ +@ThreadSafe public interface BeforeNotify { /** * Runs the "before notify" callback. If the callback returns diff --git a/sdk/src/main/java/com/bugsnag/android/BeforeRecordBreadcrumb.java b/sdk/src/main/java/com/bugsnag/android/BeforeRecordBreadcrumb.java index b15f308f67..29ae992a84 100644 --- a/sdk/src/main/java/com/bugsnag/android/BeforeRecordBreadcrumb.java +++ b/sdk/src/main/java/com/bugsnag/android/BeforeRecordBreadcrumb.java @@ -2,6 +2,8 @@ import android.support.annotation.NonNull; +import com.facebook.infer.annotation.ThreadSafe; + /** * Add a "before breadcrumb" callback, to execute code before every * breadcrumb captured by Bugsnag. @@ -17,6 +19,7 @@ * } * }) */ +@ThreadSafe public interface BeforeRecordBreadcrumb { /** diff --git a/sdk/src/main/java/com/bugsnag/android/Breadcrumb.java b/sdk/src/main/java/com/bugsnag/android/Breadcrumb.java index e5d0e2ef69..1ff823d4df 100644 --- a/sdk/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/sdk/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -61,6 +61,11 @@ public Map getMetadata() { return metadata; } + @NonNull + public String getTimestamp() { + return timestamp; + } + @Override public void toStream(@NonNull JsonStream writer) throws IOException { writer.beginObject(); diff --git a/sdk/src/main/java/com/bugsnag/android/BreadcrumbType.java b/sdk/src/main/java/com/bugsnag/android/BreadcrumbType.java index f7e14699f9..d8aaf9483e 100644 --- a/sdk/src/main/java/com/bugsnag/android/BreadcrumbType.java +++ b/sdk/src/main/java/com/bugsnag/android/BreadcrumbType.java @@ -1,8 +1,11 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + /** * Recognized types of breadcrumbs */ +@ThreadSafe public enum BreadcrumbType { /** * An error was sent to Bugsnag (internal use only) diff --git a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java index cde9c96713..980458339e 100644 --- a/sdk/src/main/java/com/bugsnag/android/Bugsnag.java +++ b/sdk/src/main/java/com/bugsnag/android/Bugsnag.java @@ -425,6 +425,7 @@ public static void notify(@NonNull final Throwable exception, final Severity sev * @param metaData additional information to send with the exception * @deprecated Use {@link #notify(Throwable, Callback)} to send and modify error reports */ + @Deprecated public static void notify(@NonNull final Throwable exception, @NonNull final MetaData metaData) { getClient().notify(exception, new Callback() { diff --git a/sdk/src/main/java/com/bugsnag/android/BugsnagException.java b/sdk/src/main/java/com/bugsnag/android/BugsnagException.java index f1ae1419a6..e5113ad29f 100644 --- a/sdk/src/main/java/com/bugsnag/android/BugsnagException.java +++ b/sdk/src/main/java/com/bugsnag/android/BugsnagException.java @@ -1,10 +1,14 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + /** * Used to store information about an exception that was not provided with an exception object */ +@ThreadSafe public class BugsnagException extends Throwable { + private static final long serialVersionUID = 5068182621179433346L; /** * The name of the exception (used instead of the exception class) */ diff --git a/sdk/src/main/java/com/bugsnag/android/Client.java b/sdk/src/main/java/com/bugsnag/android/Client.java index 63eb4e73c3..ff5727e10c 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; @@ -16,6 +18,7 @@ import android.support.annotation.Nullable; import android.text.TextUtils; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -61,14 +64,20 @@ public class Client extends Observable implements Observer { @NonNull protected final Configuration config; - private final Context appContext; - @NonNull - protected final AppData appData; + final Context appContext; + @NonNull protected final DeviceData deviceData; + + @NonNull + protected final AppData appData; + @NonNull final Breadcrumbs breadcrumbs; - protected final User user = new User(); + + @NonNull + private final User user = new User(); + @NonNull protected final ErrorStore errorStore; @@ -76,6 +85,7 @@ public class Client extends Observable implements Observer { private final EventReceiver eventReceiver; final SessionTracker sessionTracker; + SharedPreferences sharedPrefs; /** * Initialize a Bugsnag client @@ -135,11 +145,10 @@ public Client(@NonNull Context androidContext, @NonNull Configuration configurat eventReceiver = new EventReceiver(this); // Set up and collect constant app and device diagnostics - SharedPreferences sharedPref = - appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); + sharedPrefs = appContext.getSharedPreferences(SHARED_PREF_KEY, Context.MODE_PRIVATE); - appData = new AppData(appContext, config, sessionTracker); - deviceData = new DeviceData(appContext, sharedPref); + appData = new AppData(this); + deviceData = new DeviceData(this); // Set up breadcrumbs breadcrumbs = new Breadcrumbs(); @@ -147,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(sharedPref.getString(USER_ID_KEY, deviceData.getUserId())); - user.setName(sharedPref.getString(USER_NAME_KEY, null)); - user.setEmail(sharedPref.getString(USER_EMAIL_KEY, null)); + 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(deviceData.getUserId()); + user.setId(deviceId); } if (appContext instanceof Application) { @@ -209,7 +220,7 @@ public void run() { config.addObserver(this); boolean isNotProduction = !AppData.RELEASE_STAGE_PRODUCTION.equals( - AppData.guessReleaseStage(appContext)); + appData.guessReleaseStage()); Logger.setEnabled(isNotProduction); @@ -534,11 +545,40 @@ public void setUser(String id, String email, String name) { setUserName(name); } + /** + * Retrieves details of the user currently using your application. + * You can search for this information in your Bugsnag dashboard. + * + * @return the current user + */ + @NonNull + public User getUser() { + return user; + } + + @NonNull + @InternalApi + public Collection getBreadcrumbs() { + return new ArrayList<>(breadcrumbs.store); + } + + @NonNull + @InternalApi + public AppData getAppData() { + return appData; + } + + @NonNull + @InternalApi + public DeviceData getDeviceData() { + return deviceData; + } + /** * Removes the current user data and sets it back to defaults */ public void clearUser() { - user.setId(deviceData.getUserId()); + user.setId(getStringFromMap("id", deviceData.getDeviceData())); user.setEmail(null); user.setName(null); @@ -782,6 +822,7 @@ public void notify(@NonNull Throwable exception, Severity severity) { * @param metaData additional information to send with the exception * @deprecated Use {@link #notify(Throwable, Callback)} to send and modify error reports */ + @Deprecated public void notify(@NonNull Throwable exception, @NonNull MetaData metaData) { Error error = new Error.Builder(config, exception, sessionTracker.getCurrentSession()) @@ -876,14 +917,25 @@ void notify(@NonNull Error error, return; } + // generate new object each time, as this can be mutated by end-users + Map errorAppData = appData.getAppData(); + // Don't notify unless releaseStage is in notifyReleaseStages - if (!config.shouldNotifyForReleaseStage(appData.getReleaseStage())) { + String releaseStage = getStringFromMap("releaseStage", errorAppData); + + if (!config.shouldNotifyForReleaseStage(releaseStage)) { return; } // Capture the state of the app and device and attach diagnostics to the error - error.setAppData(appData); - error.setDeviceData(deviceData); + Map errorDeviceData = deviceData.getDeviceData(); + error.setDeviceData(errorDeviceData); + error.getMetaData().store.put("device", deviceData.getDeviceMetaData()); + + + // add additional info that belongs in metadata + error.setAppData(errorAppData); + error.getMetaData().store.put("app", appData.getAppDataMetaData()); // Attach breadcrumbs to the error error.setBreadcrumbs(breadcrumbs); @@ -1000,6 +1052,7 @@ public void notifyBlocking(@NonNull String name, * @param metaData additional information to send with the exception * @deprecated Use {@link #notify(Throwable, Callback)} to send and modify error reports */ + @Deprecated public void notifyBlocking(@NonNull Throwable exception, @NonNull MetaData metaData) { Error error = new Error.Builder(config, exception, sessionTracker.getCurrentSession()) diff --git a/sdk/src/main/java/com/bugsnag/android/DateUtils.java b/sdk/src/main/java/com/bugsnag/android/DateUtils.java index 899aa5ca10..1775862dbb 100644 --- a/sdk/src/main/java/com/bugsnag/android/DateUtils.java +++ b/sdk/src/main/java/com/bugsnag/android/DateUtils.java @@ -2,12 +2,15 @@ import android.support.annotation.NonNull; +import com.facebook.infer.annotation.ThreadSafe; + import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.TimeZone; +@ThreadSafe class DateUtils { // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. private static final ThreadLocal iso8601Holder = new ThreadLocal() { diff --git a/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java b/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java index a277f94c1b..1f623da31a 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java +++ b/sdk/src/main/java/com/bugsnag/android/DeliveryCompat.java @@ -1,13 +1,19 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + /** * A compatibility implementation of {@link Delivery} which wraps {@link ErrorReportApiClient} and * {@link SessionTrackingApiClient}. This class allows for backwards compatibility for users still * utilising the old API, and should be removed in the next major version. */ +@ThreadSafe class DeliveryCompat implements Delivery { + // ignore deprecation of legacy clients + @SuppressWarnings("deprecation") volatile ErrorReportApiClient errorReportApiClient; + @SuppressWarnings("deprecation") volatile SessionTrackingApiClient sessionTrackingApiClient; @Override @@ -35,6 +41,7 @@ public void deliver(Report report, Configuration config) throws DeliveryFailureE } } + @SuppressWarnings("deprecation") // ignore networkexception deprecation void handleException(Throwable throwable) throws DeliveryFailureException { if (throwable instanceof NetworkException) { throw new DeliveryFailureException(throwable.getMessage(), throwable); diff --git a/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java b/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java index 38172cb46b..95dc9ef8cc 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java +++ b/sdk/src/main/java/com/bugsnag/android/DeliveryFailureException.java @@ -1,12 +1,17 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + /** * This should be thrown if delivery of a request was not successful and you wish to try again * later. The notifier will cache the payload and initiate delivery at a future time. * * @see Delivery */ +@ThreadSafe public class DeliveryFailureException extends Exception { + private static final long serialVersionUID = 1501477209400426470L; + public DeliveryFailureException(String message) { super(message); } diff --git a/sdk/src/main/java/com/bugsnag/android/DeliveryStyle.java b/sdk/src/main/java/com/bugsnag/android/DeliveryStyle.java index c817552a11..7b5c742969 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeliveryStyle.java +++ b/sdk/src/main/java/com/bugsnag/android/DeliveryStyle.java @@ -1,5 +1,8 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + +@ThreadSafe enum DeliveryStyle { SAME_THREAD, ASYNC, diff --git a/sdk/src/main/java/com/bugsnag/android/DeviceData.java b/sdk/src/main/java/com/bugsnag/android/DeviceData.java index 39a64c53ee..9bea89259c 100644 --- a/sdk/src/main/java/com/bugsnag/android/DeviceData.java +++ b/sdk/src/main/java/com/bugsnag/android/DeviceData.java @@ -18,96 +18,135 @@ import android.support.annotation.RequiresApi; import android.util.DisplayMetrics; -import java.io.IOException; +import com.facebook.infer.annotation.ThreadSafe; + +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. - *

- * App information in this class is cached during construction for faster - * subsequent lookups and to reduce GC overhead. - */ -class DeviceData extends DeviceDataSummary { +@ThreadSafe +class DeviceData { + + 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 - final Float screenDensity; + Float screenDensity; @Nullable - final Integer dpi; + Integer dpi; @Nullable - final String screenResolution; - private Context appContext; + String screenResolution; @NonNull - final String locale; - - @Nullable - protected String id; + String locale; @NonNull - final String[] cpuAbi; + String[] cpuAbi; + + DeviceData(Client client) { + this.client = client; + this.appContext = client.appContext; + resources = appContext.getResources(); + + if (resources != null) { + displayMetrics = resources.getDisplayMetrics(); + } else { + displayMetrics = null; + } - DeviceData(@NonNull Context appContext, @NonNull SharedPreferences sharedPref) { - screenDensity = getScreenDensity(appContext); - dpi = getScreenDensityDpi(appContext); - screenResolution = getScreenResolution(appContext); - this.appContext = appContext; + screenDensity = getScreenDensity(); + dpi = getScreenDensityDpi(); + screenResolution = getScreenResolution(); locale = getLocale(); - id = retrieveUniqueInstallId(sharedPref); cpuAbi = getCpuAbi(); + emulator = isEmulator(); + id = retrieveUniqueInstallId(); } - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject(); - serialiseMinimalDeviceData(writer); - - writer - .name("id").value(id) - .name("freeMemory").value(getFreeMemory()) - .name("totalMemory").value(getTotalMemory()) - .name("freeDisk").value(getFreeDisk()) - .name("orientation").value(getOrientation(appContext)); - - - // TODO migrate metadata values - - writer - .name("batteryLevel").value(getBatteryLevel(appContext)) - .name("charging").value(isCharging(appContext)) - .name("locationStatus").value(getLocationStatus(appContext)) - .name("networkAccess").value(getNetworkAccess(appContext)) - .name("time").value(getTime()) - .name("brand").value(Build.BRAND) - .name("apiLevel").value(Build.VERSION.SDK_INT) - .name("osBuild").value(Build.DISPLAY) - .name("locale").value(locale) - .name("screenDensity").value(screenDensity) - .name("dpi").value(dpi) - .name("emulator").value(isEmulator()) - .name("screenResolution").value(screenResolution); - - writer.name("cpuAbi").beginArray(); - for (String s : cpuAbi) { - writer.value(s); - } - writer.endArray(); - writer.endObject(); + 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; } - @NonNull - String getUserId() { - return id; + 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; } - void setId(@Nullable String id) { - this.id = id; + /** + * 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; } /** @@ -126,50 +165,49 @@ private boolean isEmulator() { * The screen density scaling factor of the current Android device */ @Nullable - private static Float getScreenDensity(@NonNull Context appContext) { - Resources resources = appContext.getResources(); - if (resources == null) { + private Float getScreenDensity() { + if (displayMetrics != null) { + return displayMetrics.density; + } else { return null; } - return resources.getDisplayMetrics().density; } /** * The screen density of the current Android device in dpi, eg. 320 */ @Nullable - private static Integer getScreenDensityDpi(@NonNull Context appContext) { - Resources resources = appContext.getResources(); - if (resources == null) { + private Integer getScreenDensityDpi() { + if (displayMetrics != null) { + return displayMetrics.densityDpi; + } else { return null; } - return resources.getDisplayMetrics().densityDpi; } /** * The screen resolution of the current Android device in px, eg. 1920x1080 */ @Nullable - private static String getScreenResolution(@NonNull Context appContext) { - Resources resources = appContext.getResources(); - if (resources == null) { + 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; } - DisplayMetrics metrics = resources.getDisplayMetrics(); - int max = Math.max(metrics.widthPixels, metrics.heightPixels); - int min = Math.min(metrics.widthPixels, metrics.heightPixels); - return String.format(Locale.US, "%dx%d", max, min); } /** * Get the total memory available on the current Android device, in bytes */ - @NonNull - static Long getTotalMemory() { - if (Runtime.getRuntime().maxMemory() != Long.MAX_VALUE) { - return Runtime.getRuntime().maxMemory(); + static long calculateTotalMemory() { + Runtime runtime = Runtime.getRuntime(); + if (runtime.maxMemory() != Long.MAX_VALUE) { + return runtime.maxMemory(); } else { - return Runtime.getRuntime().totalMemory(); + return runtime.totalMemory(); } } @@ -177,7 +215,7 @@ static Long getTotalMemory() { * Get the locale of the current Android device, eg en_US */ @NonNull - private static String getLocale() { + private String getLocale() { return Locale.getDefault().toString(); } @@ -185,7 +223,8 @@ private static String getLocale() { * Get the unique id for the current app installation, creating a unique UUID if needed */ @Nullable - private String retrieveUniqueInstallId(@NonNull SharedPreferences sharedPref) { + private String retrieveUniqueInstallId() { + SharedPreferences sharedPref = client.sharedPrefs; String installId = sharedPref.getString(INSTALL_ID_KEY, null); if (installId == null) { @@ -199,39 +238,19 @@ private String retrieveUniqueInstallId(@NonNull SharedPreferences sharedPref) { * Gets information about the CPU / API */ @NonNull - private static String[] getCpuAbi() { + private String[] getCpuAbi() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { return SupportedAbiWrapper.getSupportedAbis(); } return Abi2Wrapper.getAbi1andAbi2(); } - /** - * Wrapper class to allow the test framework to use the correct version of the CPU / ABI - */ - private static class SupportedAbiWrapper { - @RequiresApi(Build.VERSION_CODES.LOLLIPOP) - public static String[] getSupportedAbis() { - return Build.SUPPORTED_ABIS; - } - } - - /** - * Wrapper class to allow the test framework to use the correct version of the CPU / ABI - */ - private static class Abi2Wrapper { - @NonNull - public static String[] getAbi1andAbi2() { - return new String[]{Build.CPU_ABI, Build.CPU_ABI2}; - } - } - - /** * Get the free disk space on the smallest disk */ @Nullable - private static Long getFreeDisk() { + @SuppressWarnings("deprecation") // ignore blockSizeLong suggestions for now (requires API 18) + private Long calculateFreeDisk() { try { StatFs externalStat = new StatFs(Environment.getExternalStorageDirectory().getPath()); long externalBytesAvailable = @@ -251,8 +270,7 @@ private static Long getFreeDisk() { /** * Get the amount of memory remaining that the VM can allocate */ - @NonNull - private static Long getFreeMemory() { + private long calculateFreeMemory() { Runtime runtime = Runtime.getRuntime(); if (runtime.maxMemory() != Long.MAX_VALUE) { return runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory(); @@ -265,18 +283,20 @@ private static Long getFreeMemory() { * Get the device orientation, eg. "landscape" */ @Nullable - private static String getOrientation(@NonNull Context appContext) { - String orientation; - switch (appContext.getResources().getConfiguration().orientation) { - case android.content.res.Configuration.ORIENTATION_LANDSCAPE: - orientation = "landscape"; - break; - case android.content.res.Configuration.ORIENTATION_PORTRAIT: - orientation = "portrait"; - break; - default: - orientation = null; - break; + 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; } @@ -285,7 +305,7 @@ private static String getOrientation(@NonNull Context appContext) { * Get the current battery charge level, eg 0.3 */ @Nullable - private static Float getBatteryLevel(@NonNull Context appContext) { + private Float getBatteryLevel() { try { IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = appContext.registerReceiver(null, ifilter); @@ -302,7 +322,7 @@ private static Float getBatteryLevel(@NonNull Context appContext) { * Is the device currently charging/full battery? */ @Nullable - private static Boolean isCharging(@NonNull Context appContext) { + private Boolean isCharging() { try { IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = appContext.registerReceiver(null, ifilter); @@ -320,7 +340,8 @@ private static Boolean isCharging(@NonNull Context appContext) { * Get the current status of location services */ @Nullable - private static String getLocationStatus(@NonNull Context appContext) { + @SuppressWarnings("deprecation") // LOCATION_PROVIDERS_ALLOWED is deprecated + private String getLocationStatus() { try { ContentResolver cr = appContext.getContentResolver(); String providersAllowed = @@ -340,7 +361,7 @@ private static String getLocationStatus(@NonNull Context appContext) { * Get the current status of network access, eg "cellular" */ @Nullable - private static String getNetworkAccess(@NonNull Context appContext) { + private String getNetworkAccess() { try { ConnectivityManager cm = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -373,4 +394,24 @@ 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 + @SuppressWarnings("deprecation") // new API already used elsewhere + 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 7b28af0a04..0000000000 --- a/sdk/src/main/java/com/bugsnag/android/DeviceDataSummary.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.bugsnag.android; - -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; - -import java.io.File; -import java.io.IOException; - -class DeviceDataSummary implements JsonStream.Streamable { - - private final Boolean rooted = isRooted(); - - @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(android.os.Build.MANUFACTURER) - .name("model").value(android.os.Build.MODEL) - .name("jailbroken").value(rooted) - .name("osName").value("android") - .name("osVersion").value(android.os.Build.VERSION.RELEASE); - } - - private static final String[] ROOT_INDICATORS = new String[]{ - // Common binaries - "/system/xbin/su", - "/system/bin/su", - // < Android 5.0 - "/system/app/Superuser.apk", - "/system/app/SuperSU.apk", - // >= Android 5.0 - "/system/app/Superuser", - "/system/app/SuperSU", - // Fallback - "/system/xbin/daemonsu", - // Systemless root - "/su/bin" - }; - - /** - * Check if the current Android device is rooted - */ - @Nullable - static Boolean isRooted() { - if (android.os.Build.TAGS != null && android.os.Build.TAGS.contains("test-keys")) { - return true; - } - - try { - for (String candidate : ROOT_INDICATORS) { - if (new File(candidate).exists()) { - return true; - } - } - } catch (Exception ignore) { - return null; - } - return false; - } - -} diff --git a/sdk/src/main/java/com/bugsnag/android/Error.java b/sdk/src/main/java/com/bugsnag/android/Error.java index 16027546be..7fd37d761f 100644 --- a/sdk/src/main/java/com/bugsnag/android/Error.java +++ b/sdk/src/main/java/com/bugsnag/android/Error.java @@ -5,6 +5,7 @@ import android.text.TextUtils; import java.io.IOException; +import java.util.Map; /** * Information and associated diagnostics relating to a handled or unhandled @@ -17,29 +18,52 @@ */ public class Error implements JsonStream.Streamable { + @SuppressWarnings("NullableProblems") // set after construction + @NonNull + private Map appData; + + @SuppressWarnings("NullableProblems") // set after construction + @NonNull + private Map deviceData; + + @SuppressWarnings("NullableProblems") // set after construction @NonNull - final Configuration config; - private AppData appData; - private DeviceData deviceData; - private Breadcrumbs breadcrumbs; private User user; - private final Throwable exception; - private Severity severity = Severity.WARNING; - @NonNull private MetaData metaData = new MetaData(); + + @Nullable + private Severity severity; + + @NonNull + private MetaData metaData = new MetaData(); + + @Nullable private String groupingHash; + + @Nullable private String context; + + @NonNull + final Configuration config; + private final String[] projectPackages; + private final Exceptions exceptions; + private Breadcrumbs breadcrumbs; + private final Throwable exception; private final HandledState handledState; private final Session session; private final ThreadState threadState; Error(@NonNull Configuration config, @NonNull Throwable exception, - HandledState handledState, Severity severity, Session session, ThreadState threadState) { + HandledState handledState, @NonNull Severity severity, + Session session, ThreadState threadState) { this.threadState = threadState; this.config = config; this.exception = exception; this.handledState = handledState; this.severity = severity; this.session = session; + + projectPackages = config.getProjectPackages(); + exceptions = new Exceptions(config, exception); } @Override @@ -56,16 +80,16 @@ public void toStream(@NonNull JsonStream writer) throws IOException { writer.name("severityReason").value(handledState); writer.name("unhandled").value(handledState.isUnhandled()); - if (config.getProjectPackages() != null) { + if (projectPackages != null) { writer.name("projectPackages").beginArray(); - for (String projectPackage : config.getProjectPackages()) { + for (String projectPackage : projectPackages) { writer.value(projectPackage); } writer.endArray(); } // Write exception info - writer.name("exceptions").value(new Exceptions(config, exception)); + writer.name("exceptions").value(exceptions); // Write user info writer.name("user").value(user); @@ -103,7 +127,7 @@ public void toStream(@NonNull JsonStream writer) throws IOException { * * @param context what was happening at the time of a crash */ - public void setContext(String context) { + public void setContext(@Nullable String context) { this.context = context; } @@ -116,11 +140,15 @@ public String getContext() { return context; } else if (config.getContext() != null) { return config.getContext(); - } else if (appData != null) { - return appData.getActiveScreenClass(); - } else { - return null; + } else if (metaData != null) { + Map app = metaData.getTab("app"); + Object activeScreen = app.get("activeScreen"); + + if (activeScreen instanceof String) { + return (String) activeScreen; + } } + return null; } /** @@ -131,10 +159,20 @@ public String getContext() { * * @param groupingHash a string to use when grouping errors */ - public void setGroupingHash(String groupingHash) { + public void setGroupingHash(@Nullable String groupingHash) { this.groupingHash = groupingHash; } + /** + * Get the grouping hash associated with this Error. + * + * @return the grouping hash, if set + */ + @Nullable + public String getGroupingHash() { + return groupingHash; + } + /** * Set the Severity of this Error. *

@@ -172,13 +210,14 @@ public void setUser(String id, String email, String name) { this.user = new User(id, email, name); } - void setUser(User user) { + void setUser(@NonNull User user) { this.user = user; } /** * @return user information associated with this Error */ + @NonNull public User getUser() { return user; } @@ -286,7 +325,8 @@ public String getExceptionName() { /** * Get the message from the exception contained in this Error report. */ - @NonNull public String getExceptionMessage() { + @NonNull + public String getExceptionMessage() { String localizedMessage = exception.getLocalizedMessage(); return localizedMessage != null ? localizedMessage : ""; } @@ -304,14 +344,34 @@ public Throwable getException() { * @param id the device id */ public void setDeviceId(@Nullable String id) { - deviceData.id = id; + deviceData.put("id", id); + } + + /** + * Retrieves the map of data associated with this error + * + * @return the app metadata + */ + @NonNull + Map getAppData() { + return appData; + } + /** + * Retrieves the {@link DeviceData} associated with this error + * + * @return the device metadata + */ + + @NonNull + public Map getDeviceData() { + return deviceData; } - void setAppData(AppData appData) { + void setAppData(@NonNull Map appData) { this.appData = appData; } - void setDeviceData(DeviceData deviceData) { + void setDeviceData(@NonNull Map deviceData) { this.deviceData = deviceData; } @@ -354,7 +414,7 @@ static class Builder { } Builder(@NonNull Configuration config, @NonNull String name, - @NonNull String message, @NonNull StackTraceElement[] frames, Session session) { + @NonNull String message, @NonNull StackTraceElement[] frames, Session session) { this(config, new BugsnagException(name, message, frames), session); } diff --git a/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java b/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java index 1c21bbb4eb..24d13dc1d7 100644 --- a/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java +++ b/sdk/src/main/java/com/bugsnag/android/ErrorReportApiClient.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + import java.util.Map; /** @@ -11,6 +13,7 @@ */ @SuppressWarnings("WeakerAccess") @Deprecated +@ThreadSafe public interface ErrorReportApiClient { /** diff --git a/sdk/src/main/java/com/bugsnag/android/ErrorStore.java b/sdk/src/main/java/com/bugsnag/android/ErrorStore.java index 119139e051..8e45b16885 100644 --- a/sdk/src/main/java/com/bugsnag/android/ErrorStore.java +++ b/sdk/src/main/java/com/bugsnag/android/ErrorStore.java @@ -2,7 +2,8 @@ import android.content.Context; import android.support.annotation.NonNull; -import android.util.Log; + +import com.facebook.infer.annotation.ThreadSafe; import java.io.File; import java.util.ArrayList; @@ -19,6 +20,7 @@ * Store and flush Error reports which couldn't be sent immediately due to * lack of network connectivity. */ +@ThreadSafe class ErrorStore extends FileStore { private static final String STARTUP_CRASH = "_startupcrash"; diff --git a/sdk/src/main/java/com/bugsnag/android/EventReceiver.java b/sdk/src/main/java/com/bugsnag/android/EventReceiver.java index 8919b4bd1d..bf877b75f6 100644 --- a/sdk/src/main/java/com/bugsnag/android/EventReceiver.java +++ b/sdk/src/main/java/com/bugsnag/android/EventReceiver.java @@ -7,9 +7,7 @@ import android.os.Bundle; import android.support.annotation.NonNull; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; /** diff --git a/sdk/src/main/java/com/bugsnag/android/ExceptionHandler.java b/sdk/src/main/java/com/bugsnag/android/ExceptionHandler.java index 835ca21eca..caad55593e 100644 --- a/sdk/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ b/sdk/src/main/java/com/bugsnag/android/ExceptionHandler.java @@ -2,7 +2,6 @@ import android.os.StrictMode; import android.support.annotation.NonNull; -import android.util.Log; import java.lang.Thread.UncaughtExceptionHandler; import java.util.Map; diff --git a/sdk/src/main/java/com/bugsnag/android/FileStore.java b/sdk/src/main/java/com/bugsnag/android/FileStore.java index 4e26717b34..fb6ce209a6 100644 --- a/sdk/src/main/java/com/bugsnag/android/FileStore.java +++ b/sdk/src/main/java/com/bugsnag/android/FileStore.java @@ -4,10 +4,11 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.facebook.infer.annotation.ThreadSafe; + import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; @@ -15,13 +16,12 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +@ThreadSafe abstract class FileStore { @NonNull diff --git a/sdk/src/main/java/com/bugsnag/android/IOUtils.java b/sdk/src/main/java/com/bugsnag/android/IOUtils.java index d5ec079c8a..ff1e845e5d 100644 --- a/sdk/src/main/java/com/bugsnag/android/IOUtils.java +++ b/sdk/src/main/java/com/bugsnag/android/IOUtils.java @@ -3,6 +3,8 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.facebook.infer.annotation.ThreadSafe; + import java.io.Closeable; import java.io.IOException; import java.io.Reader; @@ -11,6 +13,7 @@ import java.net.URLConnection; @SuppressWarnings("checkstyle:AbbreviationAsWordInName") +@ThreadSafe class IOUtils { private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; private static final int EOF = -1; diff --git a/sdk/src/main/java/com/bugsnag/android/InternalApi.java b/sdk/src/main/java/com/bugsnag/android/InternalApi.java new file mode 100644 index 0000000000..b8ce5def76 --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/InternalApi.java @@ -0,0 +1,19 @@ +package com.bugsnag.android; + +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that a method, type, or field is part of Bugsnag's internal API. Code annotated with + * this marker are subject to change without warning, regardless of their visibility. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({TYPE, FIELD, METHOD, CONSTRUCTOR}) +@interface InternalApi { +} diff --git a/sdk/src/main/java/com/bugsnag/android/JsonStream.java b/sdk/src/main/java/com/bugsnag/android/JsonStream.java index 64a7049543..d539528526 100644 --- a/sdk/src/main/java/com/bugsnag/android/JsonStream.java +++ b/sdk/src/main/java/com/bugsnag/android/JsonStream.java @@ -11,7 +11,13 @@ import java.io.Reader; import java.io.Writer; +import javax.annotation.concurrent.NotThreadSafe; + +@NotThreadSafe 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/JsonWriter.java b/sdk/src/main/java/com/bugsnag/android/JsonWriter.java index 3910f4fcab..bc3364ef12 100644 --- a/sdk/src/main/java/com/bugsnag/android/JsonWriter.java +++ b/sdk/src/main/java/com/bugsnag/android/JsonWriter.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.List; +import javax.annotation.concurrent.NotThreadSafe; + /** * Writes a JSON (RFC 4627) * encoded value to a stream, one token at a time. The stream includes both @@ -124,6 +126,7 @@ * @since 1.6 */ @SuppressWarnings({"checkstyle:AvoidEscapedUnicodeCharacters", "checkstyle:IllegalTokenText"}) +@NotThreadSafe public class JsonWriter implements Closeable { /** @@ -197,7 +200,7 @@ enum JsonScope { static { REPLACEMENT_CHARS = new String[128]; for (int i = 0; i <= 0x1f; i++) { - REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i); + REPLACEMENT_CHARS[i] = String.format("\\u%04x", i); } REPLACEMENT_CHARS['"'] = "\\\""; REPLACEMENT_CHARS['\\'] = "\\\\"; diff --git a/sdk/src/main/java/com/bugsnag/android/Logger.java b/sdk/src/main/java/com/bugsnag/android/Logger.java index 47420f9063..fe3fb22be0 100644 --- a/sdk/src/main/java/com/bugsnag/android/Logger.java +++ b/sdk/src/main/java/com/bugsnag/android/Logger.java @@ -2,6 +2,9 @@ import android.util.Log; +import com.facebook.infer.annotation.ThreadSafe; + +@ThreadSafe final class Logger { private static final String LOG_TAG = "Bugsnag"; 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..ca9be3dbfe --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/MapUtils.java @@ -0,0 +1,18 @@ +package com.bugsnag.android; + +import android.support.annotation.Nullable; + +import com.facebook.infer.annotation.ThreadSafe; + +import java.util.Map; + +@ThreadSafe +final class MapUtils { + + @Nullable + static String getStringFromMap(String key, Map map) { + Object packageName = map.get(key); + return packageName instanceof String ? (String) packageName : null; + } + +} diff --git a/sdk/src/main/java/com/bugsnag/android/MetaData.java b/sdk/src/main/java/com/bugsnag/android/MetaData.java index 258aba7797..b9f59fbdb3 100644 --- a/sdk/src/main/java/com/bugsnag/android/MetaData.java +++ b/sdk/src/main/java/com/bugsnag/android/MetaData.java @@ -4,10 +4,8 @@ import android.support.annotation.Nullable; import java.io.IOException; -import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -22,19 +20,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 +37,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); } /** @@ -103,6 +99,7 @@ public void clearTab(String tabName) { @NonNull Map getTab(String tabName) { + @SuppressWarnings("unchecked") Map tab = (Map) store.get(tabName); if (tab == null) { @@ -114,13 +111,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,18 +128,20 @@ 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)); } } } + @SuppressWarnings({"unchecked", "rawtypes"}) 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; } + @SafeVarargs @NonNull private static Map mergeMaps(@NonNull Map... maps) { Map result = new ConcurrentHashMap<>(); @@ -165,8 +164,11 @@ private static Map mergeMaps(@NonNull Map... map && baseValue instanceof Map && overridesValue instanceof Map) { // Both original and overrides are Maps, go deeper - result.put(key, mergeMaps((Map) baseValue, - (Map) overridesValue)); + @SuppressWarnings("unchecked") + Map first = (Map) baseValue; + @SuppressWarnings("unchecked") + Map second = (Map) overridesValue; + result.put(key, mergeMaps(first, second)); } else { result.put(key, overridesValue); } @@ -180,69 +182,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 e1e42ddd8d..65d85c4366 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; @@ -43,7 +45,7 @@ public static void configureClientObservers(@NonNull Client client) { // Should only happen if the NDK library is present try { String className = "com.bugsnag.android.ndk.BugsnagObserver"; - Class clz = Class.forName(className); + Class clz = Class.forName(className); Observer observer = (Observer) clz.newInstance(); client.addObserver(observer); } catch (ClassNotFoundException exception) { @@ -69,20 +71,20 @@ public static String getErrorStorePath() { } public static String getUserId() { - return getClient().user.getId(); + return getClient().getUser().getId(); } public static String getUserEmail() { - return getClient().user.getEmail(); + return getClient().getUser().getEmail(); } public static String getUserName() { - return getClient().user.getName(); + return getClient().getUser().getName(); } - @NonNull + @Nullable public static String getPackageName() { - return getClient().appData.packageName; + return getStringFromMap("packageName", getClient().appData.getAppData()); } @Nullable @@ -92,11 +94,12 @@ public static String getAppName() { @Nullable public static String getVersionName() { - return getClient().appData.versionName; + return getStringFromMap("version", getClient().appData.getAppData()); } public static int getVersionCode() { - return getClient().appData.versionCode; + Object versionCode = getClient().appData.getAppData().get("versionCode"); + return versionCode instanceof Integer ? (Integer) versionCode : -1; } @SuppressWarnings("checkstyle:AbbreviationAsWordInName") @@ -106,16 +109,16 @@ public static String getBuildUUID() { @Nullable public static String getAppVersion() { - return getClient().appData.getAppVersion(); + return getStringFromMap("version", getClient().appData.getAppData()); } public static String getReleaseStage() { - return getClient().appData.getReleaseStage(); + return getStringFromMap("releaseStage", getClient().appData.getAppData()); } @Nullable public static String getDeviceId() { - return getClient().deviceData.id; + return getStringFromMap("id", getClient().deviceData.getDeviceData()); } @NonNull @@ -124,12 +127,16 @@ public static String getDeviceLocale() { } public static double getDeviceTotalMemory() { - return DeviceData.getTotalMemory(); + return DeviceData.calculateTotalMemory(); } - @Nullable + /** + * Returns whether a device is rooted or not to the NDK + */ public static Boolean getDeviceRooted() { - return DeviceDataSummary.isRooted(); + Map map = getClient().deviceData.getDeviceDataSummary(); + Object jailbroken = map.get("jailbroken"); + return jailbroken instanceof Boolean ? (Boolean) jailbroken : false; } public static float getDeviceScreenDensity() { @@ -249,11 +256,10 @@ public void beforeNotify(@NonNull Report report) { error.config.defaultExceptionType = "c"; for (String tab : metaData.keySet()) { - Object value = metaData.get(tab); if (value instanceof Map) { - Map map = (Map) value; + @SuppressWarnings("unchecked") Map map = (Map) value; for (Object key : map.keySet()) { error.getMetaData().addToTab(tab, key.toString(), map.get(key)); diff --git a/sdk/src/main/java/com/bugsnag/android/NetworkException.java b/sdk/src/main/java/com/bugsnag/android/NetworkException.java index 6023a0fc58..3e76f0a98c 100644 --- a/sdk/src/main/java/com/bugsnag/android/NetworkException.java +++ b/sdk/src/main/java/com/bugsnag/android/NetworkException.java @@ -1,12 +1,17 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + import java.io.IOException; /** * @deprecated use {@link DeliveryFailureException} instead */ @Deprecated +@ThreadSafe public class NetworkException extends IOException { + private static final long serialVersionUID = -4370366096145029322L; + public NetworkException(String msg, Throwable cause) { super(msg, cause); } diff --git a/sdk/src/main/java/com/bugsnag/android/Notifier.java b/sdk/src/main/java/com/bugsnag/android/Notifier.java index 178fbb2b48..0d6bc78a00 100644 --- a/sdk/src/main/java/com/bugsnag/android/Notifier.java +++ b/sdk/src/main/java/com/bugsnag/android/Notifier.java @@ -8,12 +8,19 @@ * Information about this library, including name and version. */ public class Notifier implements JsonStream.Streamable { - static final String NOTIFIER_NAME = "Android Bugsnag Notifier"; - static final String NOTIFIER_VERSION = "4.5.0"; - static final String NOTIFIER_URL = "https://bugsnag.com"; - private String name; - private String version; - private String url; + + private static final String NOTIFIER_NAME = "Android Bugsnag Notifier"; + private static final String NOTIFIER_VERSION = "4.6.0"; + private static final String NOTIFIER_URL = "https://bugsnag.com"; + + @NonNull + private String name = NOTIFIER_NAME; + + @NonNull + private String version = NOTIFIER_VERSION; + + @NonNull + private String url = NOTIFIER_URL; private static final Notifier instance = new Notifier(); @@ -22,12 +29,6 @@ public static Notifier getInstance() { return instance; } - Notifier() { - this.name = NOTIFIER_NAME; - this.version = NOTIFIER_VERSION; - this.url = NOTIFIER_URL; - } - @Override public void toStream(@NonNull JsonStream writer) throws IOException { writer.beginObject(); @@ -37,16 +38,38 @@ public void toStream(@NonNull JsonStream writer) throws IOException { writer.endObject(); } + @InternalApi public void setVersion(@NonNull String version) { this.version = version; } + @InternalApi @SuppressWarnings("checkstyle:AbbreviationAsWordInName") public void setURL(@NonNull String url) { this.url = url; } + @InternalApi public void setName(@NonNull String name) { this.name = name; } + + @NonNull + @InternalApi + public String getName() { + return name; + } + + @NonNull + @InternalApi + public String getVersion() { + return version; + } + + @NonNull + @InternalApi + @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + public String getURL() { + return url; + } } diff --git a/sdk/src/main/java/com/bugsnag/android/NotifyType.java b/sdk/src/main/java/com/bugsnag/android/NotifyType.java index cba9e71706..533e12cd2a 100644 --- a/sdk/src/main/java/com/bugsnag/android/NotifyType.java +++ b/sdk/src/main/java/com/bugsnag/android/NotifyType.java @@ -2,9 +2,12 @@ import android.support.annotation.Nullable; +import com.facebook.infer.annotation.ThreadSafe; + /** * Used to inform the NDK library which type of data needs to be updated */ +@ThreadSafe public enum NotifyType { ALL(1), USER(2), 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..d809a9190d --- /dev/null +++ b/sdk/src/main/java/com/bugsnag/android/ObjectJsonStreamer.java @@ -0,0 +1,84 @@ +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()) { + + @SuppressWarnings("unchecked") + 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/Report.java b/sdk/src/main/java/com/bugsnag/android/Report.java index cc6fd0d29c..5c2cd9519e 100644 --- a/sdk/src/main/java/com/bugsnag/android/Report.java +++ b/sdk/src/main/java/com/bugsnag/android/Report.java @@ -13,11 +13,17 @@ * using your API key. */ public class Report implements JsonStream.Streamable { + @Nullable private final File errorFile; + @Nullable - private Error error; - private Notifier notifier; + private final Error error; + + @NonNull + private final Notifier notifier; + + @NonNull private String apiKey; Report(@NonNull String apiKey, @Nullable File errorFile) { @@ -51,11 +57,10 @@ public void toStream(@NonNull JsonStream writer) throws IOException { // Write in-memory event if (error != null) { writer.value(error); - } - - // Write on-disk event - if (errorFile != null) { + } else if (errorFile != null) { // Write on-disk event writer.value(errorFile); + } else { + Logger.warn("Expected error or errorFile, found empty payload instead"); } // End events array @@ -70,20 +75,45 @@ public Error getError() { return error; } + /** + * Alters the API key used for this error report. + * + * @param apiKey the new API key + */ public void setApiKey(@NonNull String apiKey) { this.apiKey = apiKey; } + /** + * @return the API key sent as part of this report. + */ + @NonNull + public String getApiKey() { + return apiKey; + } + + @InternalApi + @Deprecated public void setNotifierVersion(@NonNull String version) { notifier.setVersion(version); } + @InternalApi + @Deprecated public void setNotifierName(@NonNull String name) { notifier.setName(name); } + @InternalApi @SuppressWarnings("checkstyle:AbbreviationAsWordInName") + @Deprecated public void setNotifierURL(@NonNull String url) { notifier.setURL(url); } + + @InternalApi + @NonNull + public Notifier getNotifier() { + return notifier; + } } diff --git a/sdk/src/main/java/com/bugsnag/android/SessionStore.java b/sdk/src/main/java/com/bugsnag/android/SessionStore.java index ad084213b8..d04420483d 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionStore.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionStore.java @@ -3,6 +3,8 @@ import android.content.Context; import android.support.annotation.NonNull; +import com.facebook.infer.annotation.ThreadSafe; + import java.io.File; import java.util.Comparator; import java.util.Locale; @@ -12,6 +14,7 @@ * Store and flush Sessions which couldn't be sent immediately due to * lack of network connectivity. */ +@ThreadSafe class SessionStore extends FileStore { static final Comparator SESSION_COMPARATOR = new Comparator() { diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTracker.java b/sdk/src/main/java/com/bugsnag/android/SessionTracker.java index 740e39e17a..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; @@ -92,7 +94,8 @@ public void run() { flushStoredSessions(); SessionTrackingPayload payload = - new SessionTrackingPayload(session, client.appData); + new SessionTrackingPayload(session, null, + client.appData, client.deviceData); try { configuration.getDelivery().deliver(payload, configuration); @@ -123,7 +126,7 @@ void onAutoCaptureEnabled() { } private String getReleaseStage() { - return client.appData.getReleaseStage(); + return getStringFromMap("releaseStage", client.appData.getAppDataSummary()); } @Nullable @@ -157,7 +160,8 @@ void flushStoredSessions() { if (!storedFiles.isEmpty()) { SessionTrackingPayload payload = - new SessionTrackingPayload(storedFiles, client.appData); + new SessionTrackingPayload(null, storedFiles, + client.appData, client.deviceData); //FUTURE:SM Reduce duplication here and above try { @@ -249,7 +253,7 @@ void startFirstSession(Activity activity) { if (session == null) { long nowMs = System.currentTimeMillis(); activityFirstStartedAtMs.set(nowMs); - startNewSession(new Date(nowMs), client.user, true); + startNewSession(new Date(nowMs), client.getUser(), true); foregroundActivities.add(getActivityName(activity)); } } @@ -277,7 +281,7 @@ void updateForegroundTracker(String activityName, boolean activityStarting, long && configuration.shouldAutoCaptureSessions()) { activityFirstStartedAtMs.set(nowMs); - startNewSession(new Date(nowMs), client.user, true); + startNewSession(new Date(nowMs), client.getUser(), true); } foregroundActivities.add(activityName); } else { diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java b/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java index cabd2d6b23..322c6146aa 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTrackingApiClient.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.facebook.infer.annotation.ThreadSafe; + import java.util.Map; /** @@ -10,6 +12,7 @@ * @deprecated use {@link Delivery} to send sessions */ @Deprecated +@ThreadSafe public interface SessionTrackingApiClient { /** diff --git a/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java b/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java index 777c01306c..906b60fd47 100644 --- a/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java +++ b/sdk/src/main/java/com/bugsnag/android/SessionTrackingPayload.java @@ -4,39 +4,34 @@ import java.io.File; import java.io.IOException; -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 deviceDataSummary; + private final Map appDataSummary; private final List files; - SessionTrackingPayload(List files, AppData appData) { - this.appData = appData; - this.notifier = Notifier.getInstance(); - this.session = null; - this.files = files; - } - - SessionTrackingPayload(Session session, AppData 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(); if (session == null) { diff --git a/sdk/src/main/java/com/bugsnag/android/StrictModeHandler.java b/sdk/src/main/java/com/bugsnag/android/StrictModeHandler.java index 13783e73a4..bbbc63c86f 100644 --- a/sdk/src/main/java/com/bugsnag/android/StrictModeHandler.java +++ b/sdk/src/main/java/com/bugsnag/android/StrictModeHandler.java @@ -4,9 +4,13 @@ import android.support.annotation.Nullable; import android.text.TextUtils; +import com.facebook.infer.annotation.ThreadSafe; + import java.util.HashMap; +import java.util.Locale; import java.util.Map; +@ThreadSafe class StrictModeHandler { // Byte 1: Thread-policy (needs to be synced with StrictMode constants) @@ -26,7 +30,7 @@ class StrictModeHandler { private static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; - private static final String STRICT_MODE_CLZ_NAME = "android.os.StrictMode"; + private static final String STRICT_MODE_CLZ_NAME = "android.os.strictmode"; @SuppressLint("UseSparseArrays") private static final Map POLICY_CODE_MAP = new HashMap<>(); @@ -57,7 +61,7 @@ boolean isStrictModeThrowable(Throwable throwable) { Throwable cause = getRootCause(throwable); Class causeClass = cause.getClass(); String simpleName = causeClass.getName(); - return simpleName.startsWith(STRICT_MODE_CLZ_NAME); + return simpleName.toLowerCase(Locale.US).startsWith(STRICT_MODE_CLZ_NAME); } @Nullable diff --git a/sdk/src/main/java/com/bugsnag/android/User.java b/sdk/src/main/java/com/bugsnag/android/User.java index 743746c221..3c1b3cf95c 100644 --- a/sdk/src/main/java/com/bugsnag/android/User.java +++ b/sdk/src/main/java/com/bugsnag/android/User.java @@ -1,6 +1,7 @@ package com.bugsnag.android; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import java.io.IOException; @@ -8,14 +9,20 @@ * Information about the current user of your application. */ class User implements JsonStream.Streamable { + + @Nullable private String id; + + @Nullable private String email; + + @Nullable private String name; User() { } - User(String id, String email, String name) { + User(@Nullable String id, @Nullable String email, @Nullable String name) { this.id = id; this.email = email; this.name = name; @@ -36,27 +43,54 @@ public void toStream(@NonNull JsonStream writer) throws IOException { writer.endObject(); } + /** + * @return the user ID, by default a UUID generated on installation + */ + @Nullable public String getId() { return id; } - public void setId(String id) { + /** + * Overrides the default user ID + * + * @param id the new ID + */ + public void setId(@Nullable String id) { this.id = id; } + /** + * @return the user's email, if available + */ + @Nullable public String getEmail() { return email; } - public void setEmail(String email) { + /** + * Sets the user's email + * + * @param email the user email + */ + public void setEmail(@Nullable String email) { this.email = email; } + /** + * @return the user's name, if available + */ + @Nullable public String getName() { return name; } - public void setName(String name) { + /** + * Sets the user's name + * + * @param name the user name + */ + public void setName(@Nullable String name) { this.name = name; } }