diff --git a/.gitignore b/.gitignore
index f6b286cea9..1c5e79c122 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ out/
# Gradle files
.gradle/
build/
+/*/build/
+
# Local configuration file (sdk path, etc)
local.properties
@@ -35,6 +37,7 @@ captures/
# Intellij
*.iml
.idea/workspace.xml
+.idea/
# Keystore files
*.jks
diff --git a/analytics/build.gradle b/analytics/build.gradle
new file mode 100644
index 0000000000..396d547f71
--- /dev/null
+++ b/analytics/build.gradle
@@ -0,0 +1,45 @@
+apply plugin: 'com.android.library'
+//apply plugin: 'checkstyle'
+
+def supportLibVersion = rootProject.ext.supportLibVersion
+
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'proguard-rules.pro'
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+ lintOptions {
+ disable 'GoogleAppIndexingWarning'
+ }
+}
+
+dependencies {
+ compile project(':base')
+
+ //Mocking
+ androidTestCompile 'org.mockito:mockito-core:1.10.19'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker-dx:1.4'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'
+
+ androidTestCompile 'org.hamcrest:hamcrest-core:1.3'
+ androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
+ testCompile 'junit:junit:4.12'
+
+ androidTestCompile "com.android.support:support-annotations:$supportLibVersion"
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ androidTestCompile 'com.android.support.test:rules:0.5'
+}
\ No newline at end of file
diff --git a/analytics/proguard-rules.pro b/analytics/proguard-rules.pro
new file mode 100644
index 0000000000..7f265ec541
--- /dev/null
+++ b/analytics/proguard-rules.pro
@@ -0,0 +1,4 @@
+# The following options are set by default.
+# Make sure they are always set, even if the default proguard config changes.
+-dontskipnonpubliclibraryclasses
+-verbose
\ No newline at end of file
diff --git a/analytics/src/androidTest/java/avalanche/analytics/ApplicationTest.java b/analytics/src/androidTest/java/avalanche/analytics/ApplicationTest.java
new file mode 100644
index 0000000000..394f33b1ab
--- /dev/null
+++ b/analytics/src/androidTest/java/avalanche/analytics/ApplicationTest.java
@@ -0,0 +1,13 @@
+package avalanche.analytics;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * Testing Fundamentals
+ */
+public class ApplicationTest extends ApplicationTestCase {
+ public ApplicationTest() {
+ super(Application.class);
+ }
+}
\ No newline at end of file
diff --git a/analytics/src/main/AndroidManifest.xml b/analytics/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..cb96606808
--- /dev/null
+++ b/analytics/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/analytics/src/main/java/avalanche/analytics/ingestion/models/EventLog.java b/analytics/src/main/java/avalanche/analytics/ingestion/models/EventLog.java
new file mode 100644
index 0000000000..8a14d9d7b7
--- /dev/null
+++ b/analytics/src/main/java/avalanche/analytics/ingestion/models/EventLog.java
@@ -0,0 +1,55 @@
+package avalanche.analytics.ingestion.models;
+
+import avalanche.base.ingestion.models.InSessionLog;
+
+/**
+ * Event log.
+ */
+public class EventLog extends InSessionLog {
+
+ /**
+ * Unique identifier for this event.
+ */
+ private String id;
+
+ /**
+ * Name of the event.
+ */
+ private String name;
+
+ /**
+ * Get the id value.
+ *
+ * @return the id value
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * Set the id value.
+ *
+ * @param id the id value to set
+ */
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /**
+ * Get the name value.
+ *
+ * @return the name value
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Set the name value.
+ *
+ * @param name the name value to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/analytics/src/main/java/avalanche/analytics/ingestion/models/PageLog.java b/analytics/src/main/java/avalanche/analytics/ingestion/models/PageLog.java
new file mode 100644
index 0000000000..f7b99c021b
--- /dev/null
+++ b/analytics/src/main/java/avalanche/analytics/ingestion/models/PageLog.java
@@ -0,0 +1,32 @@
+package avalanche.analytics.ingestion.models;
+
+import avalanche.base.ingestion.models.InSessionLog;
+
+/**
+ * Page log.
+ */
+public class PageLog extends InSessionLog {
+
+ /**
+ * Name of the page.
+ */
+ private String name;
+
+ /**
+ * Get the name value.
+ *
+ * @return the name value
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Set the name value.
+ *
+ * @param name the name value to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/analytics/src/main/java/avalanche/analytics/ingestion/models/SessionLog.java b/analytics/src/main/java/avalanche/analytics/ingestion/models/SessionLog.java
new file mode 100644
index 0000000000..b314f43755
--- /dev/null
+++ b/analytics/src/main/java/avalanche/analytics/ingestion/models/SessionLog.java
@@ -0,0 +1,57 @@
+package avalanche.analytics.ingestion.models;
+
+import avalanche.base.ingestion.models.Log;
+
+/**
+ * Session log.
+ */
+public class SessionLog extends Log {
+
+ /**
+ * Unique session identifier. The same identifier must be used for end and
+ * start session.
+ */
+ private String sid;
+
+ /**
+ * `true` to mark the end of the session, `false` if it the start of the
+ * session.
+ */
+ private Boolean end;
+
+ /**
+ * Get the sid value.
+ *
+ * @return the sid value
+ */
+ public String getSid() {
+ return this.sid;
+ }
+
+ /**
+ * Set the sid value.
+ *
+ * @param sid the sid value to set
+ */
+ public void setSid(String sid) {
+ this.sid = sid;
+ }
+
+ /**
+ * Get the end value.
+ *
+ * @return the end value
+ */
+ public Boolean getEnd() {
+ return this.end;
+ }
+
+ /**
+ * Set the end value.
+ *
+ * @param end the end value to set
+ */
+ public void setEnd(Boolean end) {
+ this.end = end;
+ }
+}
diff --git a/analytics/src/main/res/values/strings.xml b/analytics/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..3b1b0facb0
--- /dev/null
+++ b/analytics/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ analytics
+
diff --git a/analytics/src/test/java/avalanche/analytics/ExampleUnitTest.java b/analytics/src/test/java/avalanche/analytics/ExampleUnitTest.java
new file mode 100644
index 0000000000..20ec876da7
--- /dev/null
+++ b/analytics/src/test/java/avalanche/analytics/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package avalanche.analytics;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/base/build.gradle b/base/build.gradle
new file mode 100644
index 0000000000..f9267aca6f
--- /dev/null
+++ b/base/build.gradle
@@ -0,0 +1,40 @@
+apply plugin: 'com.android.library'
+//apply plugin: 'checkstyle'
+
+def supportLibVersion = rootProject.ext.supportLibVersion
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'proguard-rules.pro'
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+}
+
+dependencies {
+
+ //Mocking
+ androidTestCompile 'org.mockito:mockito-core:2.0.59-beta'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker-dx:1.4'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'
+
+ androidTestCompile 'org.hamcrest:hamcrest-core:1.3'
+ androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
+ testCompile 'junit:junit:4.12'
+
+ androidTestCompile "com.android.support:support-annotations:$supportLibVersion"
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ androidTestCompile 'com.android.support.test:rules:0.5'
+}
diff --git a/base/proguard-rules.pro b/base/proguard-rules.pro
new file mode 100644
index 0000000000..7cae2dbab0
--- /dev/null
+++ b/base/proguard-rules.pro
@@ -0,0 +1,4 @@
+# The following options are set by default.
+# Make sure they are always set, even if the default proguard config changes.
+-dontskipnonpubliclibraryclasses
+-verbose
diff --git a/base/src/main/AndroidManifest.xml b/base/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..0c8246388e
--- /dev/null
+++ b/base/src/main/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/base/src/main/java/avalanche/base/AvalancheDataInterface.java b/base/src/main/java/avalanche/base/AvalancheDataInterface.java
new file mode 100644
index 0000000000..38af659c2b
--- /dev/null
+++ b/base/src/main/java/avalanche/base/AvalancheDataInterface.java
@@ -0,0 +1,12 @@
+package avalanche.base;
+
+/**
+ * Created by benny on 6/17/16.
+ */
+
+public interface AvalancheDataInterface {
+
+ //TODO could also be an enum or whatever
+ public boolean isHighPriority();
+
+}
diff --git a/base/src/main/java/avalanche/base/AvalancheFeature.java b/base/src/main/java/avalanche/base/AvalancheFeature.java
new file mode 100644
index 0000000000..cc3dd39e2e
--- /dev/null
+++ b/base/src/main/java/avalanche/base/AvalancheFeature.java
@@ -0,0 +1,9 @@
+package avalanche.base;
+
+import android.app.Application;
+
+public interface AvalancheFeature extends Application.ActivityLifecycleCallbacks {
+
+ String getName();
+
+}
diff --git a/base/src/main/java/avalanche/base/AvalancheHub.java b/base/src/main/java/avalanche/base/AvalancheHub.java
new file mode 100644
index 0000000000..6f383177b5
--- /dev/null
+++ b/base/src/main/java/avalanche/base/AvalancheHub.java
@@ -0,0 +1,245 @@
+package avalanche.base;
+
+import android.app.Application;
+
+import avalanche.base.utils.Util;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public final class AvalancheHub {
+
+ public static final String FEATURE_CRASH = "avalanche.crash.Crashes";
+
+ private static AvalancheHub sharedInstance;
+ private final Set mFeatures;
+ private String mAppIdentifier;
+ private WeakReference mApplicationWeakReference;
+
+ protected AvalancheHub() {
+ mFeatures = new HashSet<>();
+ }
+
+ public static AvalancheHub getSharedInstance() {
+ if (sharedInstance == null) {
+ sharedInstance = new AvalancheHub();
+ }
+ return sharedInstance;
+ }
+
+ /**
+ * The most convenient way to set up the SDK. It will gather all available features and automatically instantiate and use them.
+ * Instantiation and registration of the features is handled on opinionated basis - the features will register themselves in
+ * the most suitable way for them. The app identifier will be read from your manifest.
+ *
+ * @param application Your application object.
+ * @return The AvalancheHub SDK, fully configured with all features, which are available.
+ */
+ public static AvalancheHub use(Application application) {
+ return use(application, true);
+ }
+
+ /**
+ * The second-most convenient way to set up the SDK. Offers to option to skip auto configuration of the features.
+ * The app identifier will be read from your manifest.
+ *
+ * @param application Your application object.
+ * @param autoConfigure Whether to auto-use all available features. If false, only the SDK will be set up.
+ * @return The AvalancheHub SDK, ready to use.
+ */
+ public static AvalancheHub use(Application application, boolean autoConfigure) {
+ if (!autoConfigure) {
+ return use(application, new AvalancheFeature[0]);
+ }
+ String[] allFeatureNames = {FEATURE_CRASH};
+ List> features = new ArrayList<>();
+ for (String featureName : allFeatureNames) {
+ Class extends AvalancheFeature> clazz = getClassForFeature(featureName);
+ if (clazz != null) {
+ features.add(clazz);
+ }
+ }
+ //noinspection unchecked
+ return use(application, features.toArray(new Class[features.size()]));
+ }
+
+ /**
+ * Set up the SDK and provide a varargs list of feature classes you would like to have enabled and auto-configured.
+ * The app identifier will be read from your manifest.
+ *
+ * @param application Your application object.
+ * @param features Vararg list of feature classes to auto-use.
+ * @return The AvalancheHub SDK, configured with your selected features.
+ */
+ @SafeVarargs
+ public static AvalancheHub use(Application application, Class extends AvalancheFeature>... features) {
+ return use(application, Util.getAppIdentifier(application), features);
+ }
+
+ /**
+ * Set up the SDK and provide a varargs list of feature classes you would like to have enabled and auto-configured.
+ *
+ * @param application Your application object.
+ * @param appIdentifier The app identifier to use.
+ * @param features Vararg list of feature classes to auto-use.
+ * @return The AvalancheHub SDK, configured with your selected features.
+ */
+ @SafeVarargs
+ public static AvalancheHub use(Application application, String appIdentifier, Class extends AvalancheFeature>... features) {
+ List featureList = new ArrayList<>();
+ if (features != null && features.length > 0) {
+ for (Class extends AvalancheFeature> featureClass : features) {
+ AvalancheFeature feature = instantiateFeature(featureClass);
+ if (feature != null) {
+ featureList.add(feature);
+ }
+ }
+ }
+
+ return use(application, appIdentifier, featureList.toArray(new AvalancheFeature[featureList.size()]));
+ }
+
+ /**
+ * The most flexible way to set up the SDK. Configure your features first and then pass them in here to enable them in the SDK.
+ * The app identifier will be read from your manifest.
+ *
+ * @param application Your application object.
+ * @param features Vararg list of configured features to enable.
+ * @return The AvalancheHub SDK, configured with the selected feature instances.
+ */
+ public static AvalancheHub use(Application application, AvalancheFeature... features) {
+ return use(application, Util.getAppIdentifier(application), features);
+ }
+
+ /**
+ * The most flexible way to set up the SDK. Configure your features first and then pass them in here to enable them in the SDK.
+ *
+ * @param application Your application object.
+ * @param appIdentifier The app identifier to use.
+ * @param features Vararg list of configured features to enable.
+ * @return The AvalancheHub SDK, configured with the selected feature instances.
+ */
+ public static AvalancheHub use(Application application, String appIdentifier, AvalancheFeature... features) {
+ AvalancheHub avalancheHub = getSharedInstance().initialize(application, appIdentifier);
+
+ if (features != null && features.length > 0) {
+ for (AvalancheFeature feature : features) {
+ avalancheHub.addFeature(feature);
+ }
+ }
+
+ return avalancheHub;
+ }
+
+ /**
+ * Checks whether a feature is available at runtime or not.
+ *
+ * @param featureName The name of the feature you want to check for.
+ * @return Whether the feature is available.
+ */
+ public static boolean isFeatureAvailable(String featureName) {
+ return getClassForFeature(featureName) != null;
+ }
+
+ private static Class extends AvalancheFeature> getClassForFeature(String featureName) {
+ try {
+ //noinspection unchecked
+ return (Class extends AvalancheFeature>) Class.forName(featureName);
+ } catch (ClassCastException e) {
+ // If the class can be resolved but can't be cast to AvalancheFeature, this is no valid feature
+ return null;
+ } catch (ClassNotFoundException e) {
+ // If the class can not be resolved, the feature in question is not available.
+ return null;
+ }
+ }
+
+ private static AvalancheFeature instantiateFeature(Class extends AvalancheFeature> clazz) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ Method getSharedInstanceMethod = clazz.getMethod("getInstance");
+ return (AvalancheFeature) getSharedInstanceMethod.invoke(null);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private AvalancheHub initialize(Application application, String appIdentifier) {
+ mAppIdentifier = appIdentifier;
+ mApplicationWeakReference = new WeakReference<>(application);
+ mFeatures.clear();
+
+ Constants.loadFromContext(application.getApplicationContext());
+
+ return this;
+ }
+
+ /**
+ * Add and enable a configured feature.
+ *
+ * @param feature
+ */
+ public void addFeature(AvalancheFeature feature) {
+
+ Application application = getApplication();
+ if (application != null) {
+ application.registerActivityLifecycleCallbacks(feature);
+ mFeatures.add(feature);
+ }
+ }
+
+ /**
+ * Get the configured application object.
+ * @return The application instance or null if not set.
+ */
+ public Application getApplication() {
+ return mApplicationWeakReference.get();
+ }
+
+ /**
+ * Check whether a feature is enabled.
+ *
+ * @param feature Name of the feature to check.
+ * @return Whether the feature is enabled.
+ */
+ public boolean isFeatureEnabled(String feature) {
+ Class extends AvalancheFeature> clazz = getClassForFeature(feature);
+ if (clazz != null) {
+ return isFeatureEnabled(clazz);
+ }
+ return false;
+ }
+
+ /**
+ * Check whether a feature class is enabled.
+ *
+ * @param feature The feature class to check for.
+ * @return Whether the feature is enabled.
+ */
+ public boolean isFeatureEnabled(Class extends AvalancheFeature> feature) {
+ for (AvalancheFeature aFeature :
+ mFeatures) {
+ if (aFeature.getClass().equals(feature)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the configured app identifier.
+ * @return The app identifier or null if not set.
+ */
+ public String getAppIdentifier() {
+ return mAppIdentifier;
+ }
+}
diff --git a/base/src/main/java/avalanche/base/Channel.java b/base/src/main/java/avalanche/base/Channel.java
new file mode 100644
index 0000000000..030a803d87
--- /dev/null
+++ b/base/src/main/java/avalanche/base/Channel.java
@@ -0,0 +1,29 @@
+package avalanche.base;
+
+
+//TODO this class is just a dumb pipe that forwards
+public class Channel {
+
+ private static Channel sharedInstance = null;
+
+ protected Channel() {}
+
+ public static Channel getInstance() {
+ if (sharedInstance == null) {
+ sharedInstance = new Channel();
+ }
+ return sharedInstance;
+ }
+
+ public void handle(AvalancheDataInterface data) {
+ //TODO forward data to Sending/Journalling pipeline
+ if(data.isHighPriority()) {
+
+ }
+ else {
+
+ }
+ }
+
+
+}
diff --git a/base/src/main/java/avalanche/base/Constants.java b/base/src/main/java/avalanche/base/Constants.java
new file mode 100644
index 0000000000..241280b6d1
--- /dev/null
+++ b/base/src/main/java/avalanche/base/Constants.java
@@ -0,0 +1,304 @@
+package avalanche.base;
+
+import android.annotation.SuppressLint;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+
+
+import avalanche.base.utils.AvalancheLog;
+
+import java.io.File;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.UUID;
+
+/**
+ *
Description
+ *
+ * Various constants and meta information loaded from the context.
+ **/
+public class Constants {
+
+ /**
+ * AvalancheHub API URL.
+ */
+ public static final String BASE_URL = "https://sdk.hockeyapp.net/";
+ /**
+ * Name of this SDK.
+ */
+ public static final String SDK_NAME = "AvalancheSDK";
+ /**
+ * Version of the SDK - retrieved from the build configuration.
+ */
+ public static final String SDK_VERSION = BuildConfig.VERSION_NAME;
+
+ public static final String FILES_DIRECTORY_NAME = "AvalancheHub";
+
+ /**
+ * The user agent string the SDK will send with every AvalancheHub API request.
+ */
+ public static final String SDK_USER_AGENT = "AvalancheSDK/Android " + BuildConfig.VERSION_NAME;
+
+ private static final String BUNDLE_BUILD_NUMBER = "buildNumber";
+ /**
+ * Path where crash logs and temporary files are stored.
+ */
+ public static String FILES_PATH = null;
+ /**
+ * The app's version code.
+ */
+ public static String APP_VERSION = null;
+ /**
+ * The app's version name.
+ */
+ public static String APP_VERSION_NAME = null;
+ /**
+ * The app's package name.
+ */
+ public static String APP_PACKAGE = null;
+ /**
+ * The device's OS version.
+ */
+ public static String ANDROID_VERSION = null;
+ /**
+ * The device's OS build.
+ */
+ public static String ANDROID_BUILD = null;
+
+ /**
+ * The device's model name.
+ */
+ public static String PHONE_MODEL = null;
+ /**
+ * The device's model manufacturer name.
+ */
+ public static String PHONE_MANUFACTURER = null;
+ /**
+ * Unique identifier for crash reports. This is package and device specific.
+ */
+ public static String CRASH_IDENTIFIER = null;
+ /**
+ * Unique identifier for device, not dependent on package or device.
+ */
+ public static String DEVICE_IDENTIFIER = null;
+
+ /**
+ * Initializes constants from the given context. The context is used to set
+ * the package name, version code, and the files path.
+ *
+ * @param context The context to use. Usually your Activity object.
+ */
+ public static void loadFromContext(Context context) {
+ Constants.ANDROID_VERSION = Build.VERSION.RELEASE;
+ Constants.ANDROID_BUILD = Build.DISPLAY;
+ Constants.PHONE_MODEL = Build.MODEL;
+ Constants.PHONE_MANUFACTURER = Build.MANUFACTURER;
+
+ loadFilesPath(context);
+ loadPackageData(context);
+ loadCrashIdentifier(context);
+ loadDeviceIdentifier(context);
+ }
+
+ /**
+ * Returns a file representing the folder in which screenshots are stored.
+ *
+ * @return A file representing the screenshot folder.
+ */
+ public static File getAvalancheStorageDir() {
+ File externalStorage = Environment.getExternalStorageDirectory();
+
+ File dir = new File(externalStorage.getAbsolutePath() + "/" + Constants.FILES_DIRECTORY_NAME);
+ boolean success = dir.exists() || dir.mkdirs();
+ if (!success) {
+ AvalancheLog.warn("Couldn't create AvalancheHub Storage dir");
+ }
+ return dir;
+ }
+
+ /**
+ * Helper method to set the files path. If an exception occurs, the files
+ * path will be null!
+ *
+ * @param context The context to use. Usually your Activity object.
+ */
+ private static void loadFilesPath(Context context) {
+ if (context != null) {
+ try {
+ File file = context.getFilesDir();
+
+ // The file shouldn't be null, but apparently it still can happen, see
+ // http://code.google.com/p/android/issues/detail?id=8886
+ if (file != null) {
+ Constants.FILES_PATH = file.getAbsolutePath();
+ }
+ } catch (Exception e) {
+ AvalancheLog.error("Exception thrown when accessing the files dir:");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Helper method to set the package name and version code. If an exception
+ * occurs, these values will be null!
+ *
+ * @param context The context to use. Usually your Activity object.
+ */
+ private static void loadPackageData(Context context) {
+ if (context != null) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ Constants.APP_PACKAGE = packageInfo.packageName;
+ Constants.APP_VERSION = "" + packageInfo.versionCode;
+ Constants.APP_VERSION_NAME = packageInfo.versionName;
+
+ int buildNumber = loadBuildNumber(context, packageManager);
+ if ((buildNumber != 0) && (buildNumber > packageInfo.versionCode)) {
+ Constants.APP_VERSION = "" + buildNumber;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ AvalancheLog.error("Exception thrown when accessing the package info:");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Helper method to load the build number from the AndroidManifest.
+ *
+ * @param context the context to use. Usually your Activity object.
+ * @param packageManager an instance of PackageManager
+ */
+ private static int loadBuildNumber(Context context, PackageManager packageManager) {
+ try {
+ ApplicationInfo appInfo = packageManager.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
+ Bundle metaData = appInfo.metaData;
+ if (metaData != null) {
+ return metaData.getInt(BUNDLE_BUILD_NUMBER, 0);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ AvalancheLog.error("Exception thrown when accessing the application info:");
+ e.printStackTrace();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Helper method to load the crash identifier.
+ *
+ * @param context the context to use. Usually your Activity object.
+ */
+ private static void loadCrashIdentifier(Context context) {
+ String deviceIdentifier = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
+ if (!TextUtils.isEmpty(Constants.APP_PACKAGE) && !TextUtils.isEmpty(deviceIdentifier)) {
+ String combined = Constants.APP_PACKAGE + ":" + deviceIdentifier + ":" + createSalt(context);
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-1");
+ byte[] bytes = combined.getBytes("UTF-8");
+ digest.update(bytes, 0, bytes.length);
+ bytes = digest.digest();
+
+ Constants.CRASH_IDENTIFIER = bytesToHex(bytes);
+ } catch (Throwable e) {
+ AvalancheLog.error("Couldn't create CrashIdentifier with Exception:" + e.toString());
+ //TODO handle the exception
+ }
+ }
+ }
+
+ /**
+ * Helper method to generate a device identifier for telemetry and crashes,
+ *
+ * @param context The context to use. Usually your Activity object.
+ */
+ private static void loadDeviceIdentifier(Context context) {
+ // get device ID
+ ContentResolver resolver = context.getContentResolver();
+ String deviceIdentifier = Settings.Secure.getString(resolver, Settings.Secure.ANDROID_ID);
+ if (deviceIdentifier != null) {
+ String deviceIdentifierAnonymized = tryHashStringSha256(context, deviceIdentifier);
+ // if anonymized device identifier is null we should use a random UUID
+ Constants.DEVICE_IDENTIFIER = deviceIdentifierAnonymized != null ? deviceIdentifierAnonymized : UUID.randomUUID().toString();
+ }
+ }
+
+ /**
+ * Get a SHA-256 hash of the input string if the algorithm is available. If the algorithm is
+ * unavailable, return empty string.
+ *
+ * @param input the string to hash.
+ * @return a SHA-256 hash of the input or null if SHA-256 is not available (should never happen).
+ */
+ private static String tryHashStringSha256(Context context, String input) {
+ String salt = createSalt(context);
+ try {
+ // Get a Sha256 digest
+ MessageDigest hash = MessageDigest.getInstance("SHA-256");
+ hash.reset();
+ hash.update(input.getBytes());
+ hash.update(salt.getBytes());
+ byte[] hashedBytes = hash.digest();
+
+ return bytesToHex(hashedBytes);
+ } catch (NoSuchAlgorithmException e) {
+ // All android devices should support SHA256, but if unavailable return null
+ return null;
+ }
+ }
+
+ /**
+ * Helper method to create a salt for the crash identifier.
+ *
+ * @param context the context to use. Usually your Activity object.
+ */
+ @SuppressLint("InlinedApi")
+ @SuppressWarnings("deprecation")
+ private static String createSalt(Context context) {
+ String abiString;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ abiString = Build.SUPPORTED_ABIS[0];
+ } else {
+ abiString = Build.CPU_ABI;
+ }
+
+ String fingerprint = "HA" + (Build.BOARD.length() % 10) + (Build.BRAND.length() % 10) +
+ (abiString.length() % 10) + (Build.PRODUCT.length() % 10);
+ String serial = "";
+ try {
+ serial = Build.class.getField("SERIAL").get(null).toString();
+ } catch (Throwable t) {
+ }
+
+ return fingerprint + ":" + serial;
+ }
+
+ /**
+ * Helper method to convert a byte array to the hex string.
+ * Based on http://stackoverflow.com/questions/9655181/convert-from-byte-array-to-hex-string-in-java
+ *
+ * @param bytes a byte array
+ */
+ private static String bytesToHex(byte[] bytes) {
+ final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+ char[] hex = new char[bytes.length * 2];
+ for (int index = 0; index < bytes.length; index++) {
+ int value = bytes[index] & 0xFF;
+ hex[index * 2] = HEX_ARRAY[value >>> 4];
+ hex[index * 2 + 1] = HEX_ARRAY[value & 0x0F];
+ }
+ String result = new String(hex);
+ return result.replaceAll("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5");
+ }
+}
diff --git a/base/src/main/java/avalanche/base/DefaultAvalancheFeature.java b/base/src/main/java/avalanche/base/DefaultAvalancheFeature.java
new file mode 100644
index 0000000000..8f7414c350
--- /dev/null
+++ b/base/src/main/java/avalanche/base/DefaultAvalancheFeature.java
@@ -0,0 +1,47 @@
+package avalanche.base;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public abstract class DefaultAvalancheFeature implements AvalancheFeature {
+
+ @Override
+ public String getName() {
+ return getClass().getName();
+ }
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+
+ }
+
+ @Override
+ public void onActivityPaused(Activity activity) {
+
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+
+ }
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+
+ }
+}
diff --git a/base/src/main/java/avalanche/base/ingestion/models/DeviceLog.java b/base/src/main/java/avalanche/base/ingestion/models/DeviceLog.java
new file mode 100644
index 0000000000..337fba1337
--- /dev/null
+++ b/base/src/main/java/avalanche/base/ingestion/models/DeviceLog.java
@@ -0,0 +1,307 @@
+package avalanche.base.ingestion.models;
+
+/**
+ * Device characteristic log.
+ */
+public class DeviceLog extends Log {
+
+ /**
+ * Version of the SDK.
+ */
+ private String sdkVersion;
+
+ /**
+ * Device model (example: iPad2,3).
+ */
+ private String model;
+
+ /**
+ * Device manufacturer (example: HTC).
+ */
+ private String oemName;
+
+ /**
+ * OS name (example: iOS).
+ */
+ private String osName;
+
+ /**
+ * OS version (example: 9.3.0).
+ */
+ private String osVersion;
+
+ /**
+ * API level when applicable like in Android (example: 15).
+ */
+ private Integer osApiLevel;
+
+ /**
+ * Language code (example: en_US).
+ */
+ private String locale;
+
+ /**
+ * The offset in minutes from UTC for the device time zone, including
+ * daylight savings time.
+ */
+ private String timeZoneOffset;
+
+ /**
+ * Screen size of the device in pixels (example: 640x480).
+ */
+ private String screenSize;
+
+ /**
+ * Application version name.
+ */
+ private String appVersion;
+
+ /**
+ * Application version code.
+ */
+ private String appCode;
+
+ /**
+ * Carrier name (for mobile devices).
+ */
+ private String carrierName;
+
+ /**
+ * Carrier country code (for mobile devices).
+ */
+ private String carrierCountry;
+
+ /**
+ * Get the sdkVersion value.
+ *
+ * @return the sdkVersion value
+ */
+ public String getSdkVersion() {
+ return this.sdkVersion;
+ }
+
+ /**
+ * Set the sdkVersion value.
+ *
+ * @param sdkVersion the sdkVersion value to set
+ */
+ public void setSdkVersion(String sdkVersion) {
+ this.sdkVersion = sdkVersion;
+ }
+
+ /**
+ * Get the model value.
+ *
+ * @return the model value
+ */
+ public String getModel() {
+ return this.model;
+ }
+
+ /**
+ * Set the model value.
+ *
+ * @param model the model value to set
+ */
+ public void setModel(String model) {
+ this.model = model;
+ }
+
+ /**
+ * Get the oemName value.
+ *
+ * @return the oemName value
+ */
+ public String getOemName() {
+ return this.oemName;
+ }
+
+ /**
+ * Set the oemName value.
+ *
+ * @param oemName the oemName value to set
+ */
+ public void setOemName(String oemName) {
+ this.oemName = oemName;
+ }
+
+ /**
+ * Get the osName value.
+ *
+ * @return the osName value
+ */
+ public String getOsName() {
+ return this.osName;
+ }
+
+ /**
+ * Set the osName value.
+ *
+ * @param osName the osName value to set
+ */
+ public void setOsName(String osName) {
+ this.osName = osName;
+ }
+
+ /**
+ * Get the osVersion value.
+ *
+ * @return the osVersion value
+ */
+ public String getOsVersion() {
+ return this.osVersion;
+ }
+
+ /**
+ * Set the osVersion value.
+ *
+ * @param osVersion the osVersion value to set
+ */
+ public void setOsVersion(String osVersion) {
+ this.osVersion = osVersion;
+ }
+
+ /**
+ * Get the osApiLevel value.
+ *
+ * @return the osApiLevel value
+ */
+ public Integer getOsApiLevel() {
+ return this.osApiLevel;
+ }
+
+ /**
+ * Set the osApiLevel value.
+ *
+ * @param osApiLevel the osApiLevel value to set
+ */
+ public void setOsApiLevel(Integer osApiLevel) {
+ this.osApiLevel = osApiLevel;
+ }
+
+ /**
+ * Get the locale value.
+ *
+ * @return the locale value
+ */
+ public String getLocale() {
+ return this.locale;
+ }
+
+ /**
+ * Set the locale value.
+ *
+ * @param locale the locale value to set
+ */
+ public void setLocale(String locale) {
+ this.locale = locale;
+ }
+
+ /**
+ * Get the timeZoneOffset value.
+ *
+ * @return the timeZoneOffset value
+ */
+ public String getTimeZoneOffset() {
+ return this.timeZoneOffset;
+ }
+
+ /**
+ * Set the timeZoneOffset value.
+ *
+ * @param timeZoneOffset the timeZoneOffset value to set
+ */
+ public void setTimeZoneOffset(String timeZoneOffset) {
+ this.timeZoneOffset = timeZoneOffset;
+ }
+
+ /**
+ * Get the screenSize value.
+ *
+ * @return the screenSize value
+ */
+ public String getScreenSize() {
+ return this.screenSize;
+ }
+
+ /**
+ * Set the screenSize value.
+ *
+ * @param screenSize the screenSize value to set
+ */
+ public void setScreenSize(String screenSize) {
+ this.screenSize = screenSize;
+ }
+
+ /**
+ * Get the appVersion value.
+ *
+ * @return the appVersion value
+ */
+ public String getAppVersion() {
+ return this.appVersion;
+ }
+
+ /**
+ * Set the appVersion value.
+ *
+ * @param appVersion the appVersion value to set
+ */
+ public void setAppVersion(String appVersion) {
+ this.appVersion = appVersion;
+ }
+
+ /**
+ * Get the appCode value.
+ *
+ * @return the appCode value
+ */
+ public String getAppCode() {
+ return this.appCode;
+ }
+
+ /**
+ * Set the appCode value.
+ *
+ * @param appCode the appCode value to set
+ */
+ public void setAppCode(String appCode) {
+ this.appCode = appCode;
+ }
+
+ /**
+ * Get the carrierName value.
+ *
+ * @return the carrierName value
+ */
+ public String getCarrierName() {
+ return this.carrierName;
+ }
+
+ /**
+ * Set the carrierName value.
+ *
+ * @param carrierName the carrierName value to set
+ */
+ public void setCarrierName(String carrierName) {
+ this.carrierName = carrierName;
+ }
+
+ /**
+ * Get the carrierCountry value.
+ *
+ * @return the carrierCountry value
+ */
+ public String getCarrierCountry() {
+ return this.carrierCountry;
+ }
+
+ /**
+ * Set the carrierCountry value.
+ *
+ * @param carrierCountry the carrierCountry value to set
+ */
+ public void setCarrierCountry(String carrierCountry) {
+ this.carrierCountry = carrierCountry;
+ }
+}
diff --git a/base/src/main/java/avalanche/base/ingestion/models/InSessionLog.java b/base/src/main/java/avalanche/base/ingestion/models/InSessionLog.java
new file mode 100644
index 0000000000..272eb1c97d
--- /dev/null
+++ b/base/src/main/java/avalanche/base/ingestion/models/InSessionLog.java
@@ -0,0 +1,55 @@
+package avalanche.base.ingestion.models;
+
+import java.util.Map;
+
+/**
+ * The InSessionLog model.
+ */
+public abstract class InSessionLog extends Log {
+
+ /**
+ * Additional key/value pair parameters.
+ */
+ private Map properties;
+
+ /**
+ * The session identifier that was provided when the session was started.
+ */
+ private String sid;
+
+ /**
+ * Get the properties value.
+ *
+ * @return the properties value
+ */
+ public Map getProperties() {
+ return this.properties;
+ }
+
+ /**
+ * Set the properties value.
+ *
+ * @param properties the properties value to set
+ */
+ public void setProperties(Map properties) {
+ this.properties = properties;
+ }
+
+ /**
+ * Get the sid value.
+ *
+ * @return the sid value
+ */
+ public String getSid() {
+ return this.sid;
+ }
+
+ /**
+ * Set the sid value.
+ *
+ * @param sid the sid value to set
+ */
+ public void setSid(String sid) {
+ this.sid = sid;
+ }
+}
diff --git a/base/src/main/java/avalanche/base/ingestion/models/Log.java b/base/src/main/java/avalanche/base/ingestion/models/Log.java
new file mode 100644
index 0000000000..60617232b0
--- /dev/null
+++ b/base/src/main/java/avalanche/base/ingestion/models/Log.java
@@ -0,0 +1,32 @@
+package avalanche.base.ingestion.models;
+
+
+/**
+ * The Log model.
+ */
+public abstract class Log {
+
+ /**
+ * Corresponds to the number of milliseconds elapsed between the time the
+ * request is sent and the time the log is emitted.
+ */
+ private long toffset;
+
+ /**
+ * Get the toffset value.
+ *
+ * @return the toffset value
+ */
+ public long getToffset() {
+ return this.toffset;
+ }
+
+ /**
+ * Set the toffset value.
+ *
+ * @param toffset the toffset value to set
+ */
+ public void setToffset(long toffset) {
+ this.toffset = toffset;
+ }
+}
diff --git a/base/src/main/java/avalanche/base/ingestion/models/LogContainer.java b/base/src/main/java/avalanche/base/ingestion/models/LogContainer.java
new file mode 100644
index 0000000000..24a1df68dd
--- /dev/null
+++ b/base/src/main/java/avalanche/base/ingestion/models/LogContainer.java
@@ -0,0 +1,78 @@
+package avalanche.base.ingestion.models;
+
+import java.util.List;
+
+/**
+ * The LogContainer model.
+ */
+public class LogContainer {
+
+ /**
+ * Unique install identifier.
+ */
+ private String installId;
+
+ /**
+ * Application identifier.
+ */
+ private String appId;
+
+ /**
+ * The list of logs.
+ */
+ private List logs;
+
+ /**
+ * Get the installId value.
+ *
+ * @return the installId value
+ */
+ public String getInstallId() {
+ return this.installId;
+ }
+
+ /**
+ * Set the installId value.
+ *
+ * @param installId the installId value to set
+ */
+ public void setInstallId(String installId) {
+ this.installId = installId;
+ }
+
+ /**
+ * Get the appId value.
+ *
+ * @return the appId value
+ */
+ public String getAppId() {
+ return this.appId;
+ }
+
+ /**
+ * Set the appId value.
+ *
+ * @param appId the appId value to set
+ */
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ /**
+ * Get the logs value.
+ *
+ * @return the logs value
+ */
+ public List getLogs() {
+ return this.logs;
+ }
+
+ /**
+ * Set the logs value.
+ *
+ * @param logs the logs value to set
+ */
+ public void setLogs(List logs) {
+ this.logs = logs;
+ }
+}
diff --git a/base/src/main/java/avalanche/base/utils/AsyncTaskUtils.java b/base/src/main/java/avalanche/base/utils/AsyncTaskUtils.java
new file mode 100644
index 0000000000..353333760d
--- /dev/null
+++ b/base/src/main/java/avalanche/base/utils/AsyncTaskUtils.java
@@ -0,0 +1,29 @@
+package avalanche.base.utils;
+
+import android.os.AsyncTask;
+
+import java.util.concurrent.Executor;
+
+/**
+ *
Description
+ *
+ * Either calls execute or executeOnExecutor on an AsyncTask depending on the
+ * API level.
+ */
+public class AsyncTaskUtils {
+
+ private static Executor sCustomExecutor;
+
+ public static void execute(AsyncTask asyncTask) {
+ asyncTask.executeOnExecutor(sCustomExecutor != null ? sCustomExecutor : AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public static Executor getCustomExecutor() {
+ return sCustomExecutor;
+ }
+
+ public static void setCustomExecutor(Executor customExecutor) {
+ sCustomExecutor = customExecutor;
+ }
+
+}
diff --git a/base/src/main/java/avalanche/base/utils/AvalancheLog.java b/base/src/main/java/avalanche/base/utils/AvalancheLog.java
new file mode 100644
index 0000000000..656e8017b2
--- /dev/null
+++ b/base/src/main/java/avalanche/base/utils/AvalancheLog.java
@@ -0,0 +1,285 @@
+package avalanche.base.utils;
+
+
+import android.util.Log;
+
+/**
+ *
Description
+ *
+ * Wrapper class for logging in the SDK as well as
+ * setting the desired log level for end users.
+ * Log levels correspond to those of android.util.Log.
+ *
+ * @see Log
+ */
+public class AvalancheLog {
+ public static final String AVALANCHE_TAG = "AvalancheHub";
+
+ private static int sLogLevel = Log.ERROR;
+
+ /**
+ * Get the loglevel to find out how much data the AvalancheSDK spews into LogCat. The Default will be
+ * LOG_LEVEL.ERROR so only errors show up in LogCat.
+ *
+ * @return the log level
+ */
+ public static int getLogLevel() {
+ return sLogLevel;
+ }
+
+ /**
+ * Set the log level to determine the amount of info the AvalancheSDK spews info into LogCat.
+ *
+ * @param avalancheLogLevel The log level for AvalancheSDK logging
+ */
+ public static void setLogLevel(int avalancheLogLevel) {
+ sLogLevel = avalancheLogLevel;
+ }
+
+
+ /**
+ * Log a message with level VERBOSE with the default tag
+ *
+ * @param message the log message
+ */
+ public static void verbose(String message) {
+ verbose(null, message);
+ }
+
+ /**
+ * Log a message with level VERBOSE
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ */
+ public static void verbose(String tag, String message) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.VERBOSE) {
+ Log.v(tag, message);
+ }
+ }
+
+ /**
+ * Log a message with level VERBOSE with the default tag
+ *
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void verbose(String message, Throwable throwable) {
+ verbose(null, message, throwable);
+ }
+
+ /**
+ * Log a message with level VERBOSE
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void verbose(String tag, String message, Throwable throwable) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.VERBOSE) {
+ Log.v(tag, message, throwable);
+ }
+ }
+
+ /**
+ * Log a message with level DEBUG with the default tag
+ *
+ * @param message the log message
+ */
+ public static void debug(String message) {
+ debug(null, message);
+ }
+
+ /**
+ * Log a message with level DEBUG
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ */
+ public static void debug(String tag, String message) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.DEBUG) {
+ Log.d(tag, message);
+ }
+ }
+
+ /**
+ * Log a message with level DEBUG with the default tag
+ *
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void debug(String message, Throwable throwable) {
+ debug(null, message, throwable);
+ }
+
+ /**
+ * Log a message with level DEBUG
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void debug(String tag, String message, Throwable throwable) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.DEBUG) {
+ Log.d(tag, message, throwable);
+ }
+ }
+
+ /**
+ * Log a message with level INFO with the default tag
+ *
+ * @param message the log message
+ */
+ public static void info(String message) {
+ info(null, message);
+ }
+
+ /**
+ * Log a message with level INFO
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ */
+ public static void info(String tag, String message) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.INFO) {
+ Log.i(tag, message);
+ }
+ }
+
+ /**
+ * Log a message with level INFO with the default tag
+ *
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void info(String message, Throwable throwable) {
+ info(message, throwable);
+ }
+
+ /**
+ * Log a message with level INFO
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void info(String tag, String message, Throwable throwable) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.INFO) {
+ Log.i(tag, message, throwable);
+ }
+ }
+
+ /**
+ * Log a message with level WARN with the default tag
+ *
+ * @param message the log message
+ */
+ public static void warn(String message) {
+ warn(null, message);
+ }
+
+ /**
+ * Log a message with level WARN
+ *
+ * @param tag the TAG
+ * @param message the log message
+ */
+ public static void warn(String tag, String message) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.WARN) {
+ Log.w(tag, message);
+ }
+ }
+
+ /**
+ * Log a message with level WARN with the default tag
+ *
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void warn(String message, Throwable throwable) {
+ warn(null, message, throwable);
+ }
+
+ /**
+ * Log a message with level WARN
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void warn(String tag, String message, Throwable throwable) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.WARN) {
+ Log.w(tag, message, throwable);
+ }
+ }
+
+ /**
+ * Log a message with level ERROR with the default tag
+ *
+ * @param message the log message
+ */
+ public static void error(String message) {
+ error(null, message);
+ }
+
+ /**
+ * Log a message with level ERROR
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ */
+ public static void error(String tag, String message) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.ERROR) {
+ Log.e(tag, message);
+ }
+ }
+
+ /**
+ * Log a message with level ERROR with the default tag
+ *
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void error(String message, Throwable throwable) {
+ error(null, message, throwable);
+ }
+
+ /**
+ * Log a message with level ERROR
+ *
+ * @param tag the log tag for your message
+ * @param message the log message
+ * @param throwable the throwable you want to log
+ */
+ public static void error(String tag, String message, Throwable throwable) {
+ tag = sanitizeTag(tag);
+ if (sLogLevel <= Log.ERROR) {
+ Log.e(tag, message, throwable);
+ }
+ }
+
+ /**
+ * Sanitize a TAG string
+ *
+ * @param tag the log tag for your message for the logging
+ * @return a sanitized TAG, defaults to 'AvalancheHub' in case the log tag for your message is null, empty or longer than
+ * 23 characters.
+ */
+ static String sanitizeTag(String tag) {
+ if ((tag == null) || (tag.length() == 0) || (tag.length() > 23)) {
+ tag = AVALANCHE_TAG;
+ }
+
+ return tag;
+ }
+
+}
diff --git a/base/src/main/java/avalanche/base/utils/Base64.java b/base/src/main/java/avalanche/base/utils/Base64.java
new file mode 100644
index 0000000000..984320e73a
--- /dev/null
+++ b/base/src/main/java/avalanche/base/utils/Base64.java
@@ -0,0 +1,745 @@
+package avalanche.base.utils;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ *
Description
+ *
+ * Utilities for encoding and decoding the Base64 representation of
+ * binary data. See RFCs 2045 and 3548.
+ *
+ */
+public class Base64 {
+ private static final String TAG = "BASE64";
+
+ //REMOVE AS ANDROID
+
+
+ /**
+ * Default values for encoder/decoder flags.
+ */
+ public static final int DEFAULT = 0;
+
+ /**
+ * Encoder flag bit to omit the padding '=' characters at the end
+ * of the output (if any).
+ */
+ public static final int NO_PADDING = 1;
+
+ /**
+ * Encoder flag bit to omit all line terminators (i.e., the output
+ * will be on one long line).
+ */
+ public static final int NO_WRAP = 2;
+
+ /**
+ * Encoder flag bit to indicate lines should be terminated with a
+ * CRLF pair instead of just an LF. Has no effect if {@code
+ * NO_WRAP} is specified as well.
+ */
+ public static final int CRLF = 4;
+
+ /**
+ * Encoder/decoder flag bit to indicate using the "URL and
+ * filename safe" variant of Base64 (see RFC 3548 section 4) where
+ * {@code -} and {@code _} are used in place of {@code +} and
+ * {@code /}.
+ */
+ public static final int URL_SAFE = 8;
+
+ // --------------------------------------------------------
+ // shared code
+ // --------------------------------------------------------
+
+ /* package */ static abstract class Coder {
+ public byte[] output;
+ public int op;
+
+ /**
+ * Encode/decode another block of input data. this.output is
+ * provided by the caller, and must be big enough to hold all
+ * the coded data. On exit, this.opwill be set to the length
+ * of the coded data.
+ *
+ * @param finish true if this is the final call to process for
+ * this object. Will finalize the coder state and
+ * include any final bytes in the output.
+ * @return true if the input so far is good; false if some
+ * error has been detected in the input stream..
+ */
+ public abstract boolean process(byte[] input, int offset, int len, boolean finish);
+
+ /**
+ * @return the maximum number of bytes a call to process()
+ * could produce for the given number of input bytes. This may
+ * be an overestimate.
+ */
+ public abstract int maxOutputSize(int len);
+ }
+
+ // --------------------------------------------------------
+ // decoding
+ // --------------------------------------------------------
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param str the input String to decode, which is converted to
+ * bytes using the default charset
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ * @return the decoded data as a byte array
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(String str, int flags) {
+ return decode(str.getBytes(), flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the input array to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ * @return the decoded data as a byte array
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int flags) {
+ return decode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Decode the Base64-encoded data in input and return the data in
+ * a new byte array.
+ *
+ *
The padding '=' characters at the end are considered optional, but
+ * if any are present, there must be the correct number of them.
+ *
+ * @param input the data to decode
+ * @param offset the position within the input array at which to start
+ * @param len the number of bytes of input to decode
+ * @param flags controls certain features of the decoded output.
+ * Pass {@code DEFAULT} to decode standard Base64.
+ * @return the decoded data as a byte array
+ * @throws IllegalArgumentException if the input contains
+ * incorrect padding
+ */
+ public static byte[] decode(byte[] input, int offset, int len, int flags) {
+ // Allocate space for the most data the input could represent.
+ // (It could contain less if it contains whitespace, etc.)
+ Decoder decoder = new Decoder(flags, new byte[len * 3 / 4]);
+
+ if (!decoder.process(input, offset, len, true)) {
+ throw new IllegalArgumentException("bad base-64");
+ }
+
+ // Maybe we got lucky and allocated exactly enough output space.
+ if (decoder.op == decoder.output.length) {
+ return decoder.output;
+ }
+
+ // Need to shorten the array, so allocate a new one of the
+ // right size and copy.
+ byte[] temp = new byte[decoder.op];
+ System.arraycopy(decoder.output, 0, temp, 0, decoder.op);
+ return temp;
+ }
+
+ /* package */ static class Decoder extends Coder {
+ /**
+ * Lookup table for turning bytes into their position in the
+ * Base64 alphabet.
+ */
+ private static final int DECODE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Decode lookup table for the "web safe" variant (RFC 3548
+ * sec. 4) where - and _ replace + and /.
+ */
+ private static final int DECODE_WEBSAFE[] = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1,
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1,
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63,
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ };
+
+ /**
+ * Non-data values in the DECODE arrays.
+ */
+ private static final int SKIP = -1;
+ private static final int EQUALS = -2;
+
+ /**
+ * States 0-3 are reading through the next input tuple.
+ * State 4 is having read one '=' and expecting exactly
+ * one more.
+ * State 5 is expecting no more data or padding characters
+ * in the input.
+ * State 6 is the error state; an error has been detected
+ * in the input and no future input can "fix" it.
+ */
+ private int state; // state number (0 to 6)
+ private int value;
+
+ final private int[] alphabet;
+
+ public Decoder(int flags, byte[] output) {
+ this.output = output;
+
+ alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE;
+ state = 0;
+ value = 0;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could decode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 3 / 4 + 10;
+ }
+
+ /**
+ * Decode another block of input data.
+ *
+ * @return true if the state machine is still healthy. false if
+ * bad base-64 data has been detected in the input stream.
+ */
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ if (this.state == 6) return false;
+
+ int p = offset;
+ len += offset;
+
+ // Using local variables makes the decoder about 12%
+ // faster than if we manipulate the member variables in
+ // the loop. (Even alphabet makes a measurable
+ // difference, which is somewhat surprising to me since
+ // the member variable is final.)
+ int state = this.state;
+ int value = this.value;
+ int op = 0;
+ final byte[] output = this.output;
+ final int[] alphabet = this.alphabet;
+
+ while (p < len) {
+ // Try the fast path: we're starting a new tuple and the
+ // next four bytes of the input stream are all data
+ // bytes. This corresponds to going through states
+ // 0-1-2-3-0. We expect to use this method for most of
+ // the data.
+ //
+ // If any of the next four bytes of input are non-data
+ // (whitespace, etc.), value will end up negative. (All
+ // the non-data values in decode are small negative
+ // numbers, so shifting any of them up and or'ing them
+ // together will result in a value with its top bit set.)
+ //
+ // You can remove this whole block and the output should
+ // be the same, just slower.
+ if (state == 0) {
+ while (p + 4 <= len &&
+ (value = ((alphabet[input[p] & 0xff] << 18) |
+ (alphabet[input[p + 1] & 0xff] << 12) |
+ (alphabet[input[p + 2] & 0xff] << 6) |
+ (alphabet[input[p + 3] & 0xff]))) >= 0) {
+ output[op + 2] = (byte) value;
+ output[op + 1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ p += 4;
+ }
+ if (p >= len) break;
+ }
+
+ // The fast path isn't available -- either we've read a
+ // partial tuple, or the next four input bytes aren't all
+ // data, or whatever. Fall back to the slower state
+ // machine implementation.
+
+ int d = alphabet[input[p++] & 0xff];
+
+ switch (state) {
+ case 0:
+ if (d >= 0) {
+ value = d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 1:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 2:
+ if (d >= 0) {
+ value = (value << 6) | d;
+ ++state;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect exactly one more padding character.
+ output[op++] = (byte) (value >> 4);
+ state = 4;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 3:
+ if (d >= 0) {
+ // Emit the output triple and return to state 0.
+ value = (value << 6) | d;
+ output[op + 2] = (byte) value;
+ output[op + 1] = (byte) (value >> 8);
+ output[op] = (byte) (value >> 16);
+ op += 3;
+ state = 0;
+ } else if (d == EQUALS) {
+ // Emit the last (partial) output tuple;
+ // expect no further data or padding characters.
+ output[op + 1] = (byte) (value >> 2);
+ output[op] = (byte) (value >> 10);
+ op += 2;
+ state = 5;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 4:
+ if (d == EQUALS) {
+ ++state;
+ } else if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+
+ case 5:
+ if (d != SKIP) {
+ this.state = 6;
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (!finish) {
+ // We're out of input, but a future call could provide
+ // more.
+ this.state = state;
+ this.value = value;
+ this.op = op;
+ return true;
+ }
+
+ // Done reading input. Now figure out where we are left in
+ // the state machine and finish up.
+
+ switch (state) {
+ case 0:
+ // Output length is a multiple of three. Fine.
+ break;
+ case 1:
+ // Read one extra input byte, which isn't enough to
+ // make another output byte. Illegal.
+ this.state = 6;
+ return false;
+ case 2:
+ // Read two extra input bytes, enough to emit 1 more
+ // output byte. Fine.
+ output[op++] = (byte) (value >> 4);
+ break;
+ case 3:
+ // Read three extra input bytes, enough to emit 2 more
+ // output bytes. Fine.
+ output[op++] = (byte) (value >> 10);
+ output[op++] = (byte) (value >> 2);
+ break;
+ case 4:
+ // Read one padding '=' when we expected 2. Illegal.
+ this.state = 6;
+ return false;
+ case 5:
+ // Read all the padding '='s we expected and no more.
+ // Fine.
+ break;
+ }
+
+ this.state = state;
+ this.op = op;
+ return true;
+ }
+ }
+
+ // --------------------------------------------------------
+ // encoding
+ // --------------------------------------------------------
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ * @return the encoded data as a string
+ */
+ public static String encodeToString(byte[] input, int flags) {
+ try {
+ return new String(encode(input, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * String with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ * @return the encoded data as a string
+ */
+ public static String encodeToString(byte[] input, int offset, int len, int flags) {
+ try {
+ return new String(encode(input, offset, len, flags), "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ // US-ASCII is guaranteed to be available.
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ * @return the encoded data as a byte array
+ */
+ public static byte[] encode(byte[] input, int flags) {
+ return encode(input, 0, input.length, flags);
+ }
+
+ /**
+ * Base64-encode the given data and return a newly allocated
+ * byte[] with the result.
+ *
+ * @param input the data to encode
+ * @param offset the position within the input array at which to
+ * start
+ * @param len the number of bytes of input to encode
+ * @param flags controls certain features of the encoded output.
+ * Passing {@code DEFAULT} results in output that
+ * adheres to RFC 2045.
+ * @return the encoded data as a byte array
+ */
+ public static byte[] encode(byte[] input, int offset, int len, int flags) {
+ Encoder encoder = new Encoder(flags, null);
+
+ // Compute the exact length of the array we will produce.
+ int output_len = len / 3 * 4;
+
+ // Account for the tail of the data and the padding bytes, if any.
+ if (encoder.do_padding) {
+ if (len % 3 > 0) {
+ output_len += 4;
+ }
+ } else {
+ switch (len % 3) {
+ case 0:
+ break;
+ case 1:
+ output_len += 2;
+ break;
+ case 2:
+ output_len += 3;
+ break;
+ }
+ }
+
+ // Account for the newlines, if any.
+ if (encoder.do_newline && len > 0) {
+ output_len += (((len - 1) / (3 * Encoder.LINE_GROUPS)) + 1) *
+ (encoder.do_cr ? 2 : 1);
+ }
+
+ encoder.output = new byte[output_len];
+ encoder.process(input, offset, len, true);
+
+ if (encoder.op != output_len) {
+ throw new AssertionError();
+ }
+
+ return encoder.output;
+ }
+
+ /* package */ static class Encoder extends Coder {
+ /**
+ * Emit a new line every this many output tuples. Corresponds to
+ * a 76-character line length (the maximum allowable according to
+ * RFC 2045).
+ */
+ public static final int LINE_GROUPS = 19;
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
+ };
+
+ /**
+ * Lookup table for turning Base64 alphabet positions (6 bits)
+ * into output bytes.
+ */
+ private static final byte ENCODE_WEBSAFE[] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
+ 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
+ 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
+ };
+
+ final private byte[] tail;
+ /* package */ int tailLen;
+ private int count;
+
+ final public boolean do_padding;
+ final public boolean do_newline;
+ final public boolean do_cr;
+ final private byte[] alphabet;
+
+ public Encoder(int flags, byte[] output) {
+ this.output = output;
+
+ do_padding = (flags & NO_PADDING) == 0;
+ do_newline = (flags & NO_WRAP) == 0;
+ do_cr = (flags & CRLF) != 0;
+ alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE;
+
+ tail = new byte[2];
+ tailLen = 0;
+
+ count = do_newline ? LINE_GROUPS : -1;
+ }
+
+ /**
+ * @return an overestimate for the number of bytes {@code
+ * len} bytes could encode to.
+ */
+ public int maxOutputSize(int len) {
+ return len * 8 / 5 + 10;
+ }
+
+ public boolean process(byte[] input, int offset, int len, boolean finish) {
+ // Using local variables makes the encoder about 9% faster.
+ final byte[] alphabet = this.alphabet;
+ final byte[] output = this.output;
+ int op = 0;
+ int count = this.count;
+
+ int p = offset;
+ len += offset;
+ int v = -1;
+
+ // First we need to concatenate the tail of the previous call
+ // with any input bytes available now and see if we can empty
+ // the tail.
+
+ switch (tailLen) {
+ case 0:
+ // There was no tail.
+ break;
+
+ case 1:
+ if (p + 2 <= len) {
+ // A 1-byte tail with at least 2 bytes of
+ // input available now.
+ v = ((tail[0] & 0xff) << 16) |
+ ((input[p++] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+
+ case 2:
+ if (p + 1 <= len) {
+ // A 2-byte tail with at least 1 byte of input.
+ v = ((tail[0] & 0xff) << 16) |
+ ((tail[1] & 0xff) << 8) |
+ (input[p++] & 0xff);
+ tailLen = 0;
+ }
+ break;
+ }
+
+ if (v != -1) {
+ output[op++] = alphabet[(v >> 18) & 0x3f];
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ // At this point either there is no tail, or there are fewer
+ // than 3 bytes of input available.
+
+ // The main loop, turning 3 input bytes into 4 output bytes on
+ // each iteration.
+ while (p + 3 <= len) {
+ v = ((input[p] & 0xff) << 16) |
+ ((input[p + 1] & 0xff) << 8) |
+ (input[p + 2] & 0xff);
+ output[op] = alphabet[(v >> 18) & 0x3f];
+ output[op + 1] = alphabet[(v >> 12) & 0x3f];
+ output[op + 2] = alphabet[(v >> 6) & 0x3f];
+ output[op + 3] = alphabet[v & 0x3f];
+ p += 3;
+ op += 4;
+ if (--count == 0) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ count = LINE_GROUPS;
+ }
+ }
+
+ if (finish) {
+ // Finish up the tail of the input. Note that we need to
+ // consume any bytes in tail before any bytes
+ // remaining in input; there should be at most two bytes
+ // total.
+
+ if (p - tailLen == len - 1) {
+ int t = 0;
+ v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4;
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (p - tailLen == len - 2) {
+ int t = 0;
+ v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) |
+ (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2);
+ tailLen -= t;
+ output[op++] = alphabet[(v >> 12) & 0x3f];
+ output[op++] = alphabet[(v >> 6) & 0x3f];
+ output[op++] = alphabet[v & 0x3f];
+ if (do_padding) {
+ output[op++] = '=';
+ }
+ if (do_newline) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+ } else if (do_newline && op > 0 && count != LINE_GROUPS) {
+ if (do_cr) output[op++] = '\r';
+ output[op++] = '\n';
+ }
+
+ if (tailLen != 0) {
+ AvalancheLog.error(TAG, "Error during encoding");
+ //throw new AssertionError();
+ }
+ if (p != len) {
+ AvalancheLog.error(TAG, "Error during encoding");
+ //throw new AssertionError();
+ }
+ } else {
+ // Save the leftovers in tail to be consumed on the next
+ // call to encodeInternal.
+
+ if (p == len - 1) {
+ tail[tailLen++] = input[p];
+ } else if (p == len - 2) {
+ tail[tailLen++] = input[p];
+ tail[tailLen++] = input[p + 1];
+ }
+ }
+
+ this.op = op;
+ this.count = count;
+
+ return true;
+ }
+ }
+
+ private Base64() {
+ } // don't instantiate
+}
diff --git a/base/src/main/java/avalanche/base/utils/HttpURLConnectionBuilder.java b/base/src/main/java/avalanche/base/utils/HttpURLConnectionBuilder.java
new file mode 100644
index 0000000000..4b0a37492f
--- /dev/null
+++ b/base/src/main/java/avalanche/base/utils/HttpURLConnectionBuilder.java
@@ -0,0 +1,162 @@
+package avalanche.base.utils;
+
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+import avalanche.base.Constants;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+
+
+
+
+ a
+ b
+
+
+
diff --git a/crash/build.gradle b/crash/build.gradle
new file mode 100644
index 0000000000..396d547f71
--- /dev/null
+++ b/crash/build.gradle
@@ -0,0 +1,45 @@
+apply plugin: 'com.android.library'
+//apply plugin: 'checkstyle'
+
+def supportLibVersion = rootProject.ext.supportLibVersion
+
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles 'proguard-rules.pro'
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ }
+ }
+ lintOptions {
+ disable 'GoogleAppIndexingWarning'
+ }
+}
+
+dependencies {
+ compile project(':base')
+
+ //Mocking
+ androidTestCompile 'org.mockito:mockito-core:1.10.19'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker:1.4'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker-dx:1.4'
+ androidTestCompile 'com.crittercism.dexmaker:dexmaker-mockito:1.4'
+
+ androidTestCompile 'org.hamcrest:hamcrest-core:1.3'
+ androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
+ testCompile 'junit:junit:4.12'
+
+ androidTestCompile "com.android.support:support-annotations:$supportLibVersion"
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ androidTestCompile 'com.android.support.test:rules:0.5'
+}
\ No newline at end of file
diff --git a/crash/proguard-rules.pro b/crash/proguard-rules.pro
new file mode 100644
index 0000000000..7f265ec541
--- /dev/null
+++ b/crash/proguard-rules.pro
@@ -0,0 +1,4 @@
+# The following options are set by default.
+# Make sure they are always set, even if the default proguard config changes.
+-dontskipnonpubliclibraryclasses
+-verbose
\ No newline at end of file
diff --git a/crash/src/main/AndroidManifest.xml b/crash/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..4d1bdc602e
--- /dev/null
+++ b/crash/src/main/AndroidManifest.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/crash/src/main/java/avalanche/crash/Crashes.java b/crash/src/main/java/avalanche/crash/Crashes.java
new file mode 100644
index 0000000000..de26fee0a1
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/Crashes.java
@@ -0,0 +1,647 @@
+package avalanche.crash;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import avalanche.base.AvalancheHub;
+import avalanche.base.Constants;
+import avalanche.base.DefaultAvalancheFeature;
+
+
+
+import avalanche.crash.model.CrashReport;
+import avalanche.crash.model.CrashesUserInput;
+import avalanche.crash.model.CrashMetaData;
+import avalanche.base.utils.AvalancheLog;
+import avalanche.base.utils.HttpURLConnectionBuilder;
+import avalanche.base.utils.Util;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.net.HttpURLConnection;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static android.text.TextUtils.isEmpty;
+
+
+public class Crashes extends DefaultAvalancheFeature {
+
+ private static final String ALWAYS_SEND_KEY = "always_send_crash_reports";
+
+ private static final int STACK_TRACES_FOUND_NONE = 0;
+ private static final int STACK_TRACES_FOUND_NEW = 1;
+ private static final int STACK_TRACES_FOUND_CONFIRMED = 2;
+ private static Crashes sharedInstance = null;
+ private CrashesListener mListener;
+ private String mEndpointUrl;
+ private WeakReference mContextWeakReference;
+ private boolean mLazyExecution;
+ private boolean mIsSubmitting = false;
+ private long mInitializeTimestamp;
+ private boolean mDidCrashInLastSession = false;
+
+ protected Crashes() {
+ }
+
+ public static Crashes getInstance() {
+ if (sharedInstance == null) {
+ sharedInstance = new Crashes();
+ }
+ return sharedInstance;
+ }
+
+ /**
+ * Searches .stacktrace files and returns them as array.
+ */
+ private static String[] searchForStackTraces() {
+ if (Constants.FILES_PATH != null) {
+ AvalancheLog.debug("Looking for exceptions in: " + Constants.FILES_PATH);
+
+ // Try to create the files folder if it doesn't exist
+ File dir = new File(Constants.FILES_PATH + "/");
+ boolean created = dir.mkdir();
+ if (!created && !dir.exists()) {
+ return new String[0];
+ }
+
+ // Filter for ".stacktrace" files
+ FilenameFilter filter = new FilenameFilter() {
+ public boolean accept(File dir, String name) {
+ return name.endsWith(".stacktrace");
+ }
+ };
+ return dir.list(filter);
+ } else {
+ AvalancheLog.debug("Can't search for exception as file path is null.");
+ return null;
+ }
+ }
+
+ private static List getConfirmedFilenames(Context context) {
+ List result = null;
+ if (context != null) {
+ SharedPreferences preferences = context.getSharedPreferences("AvalancheSDK", Context.MODE_PRIVATE);
+ result = Arrays.asList(preferences.getString("ConfirmedFilenames", "").split("\\|"));
+ }
+ return result;
+ }
+
+ private static String getAlertTitle(Context context) {
+ String appTitle = Util.getAppName(context);
+
+ String message = context.getString(R.string.avalanche_crash_dialog_title);
+ return String.format(message, appTitle);
+ }
+
+ @Override
+ public void onActivityResumed(Activity activity) {
+ super.onActivityResumed(activity);
+ if (mContextWeakReference == null && Util.isMainActivity(activity)) {
+ // Opinionated approach -> per default we will want to activate the crash reporting with the very first of your activities.
+ register(activity);
+ }
+ }
+
+ /**
+ * Registers the crash manager and handles existing crash logs.
+ * AvalancheHub app identifier is read from configuration values in manifest.
+ *
+ * @param context The context to use. Usually your Activity object. If
+ * context is not an instance of Activity (or a subclass of it),
+ * crashes will be sent automatically.
+ * @return The configured crash manager for method chaining.
+ */
+ public Crashes register(Context context) {
+ return register(context, null);
+ }
+
+ /**
+ * Registers the crash manager and handles existing crash logs.
+ * AvalancheHub app identifier is read from configuration values in manifest.
+ *
+ * @param context The context to use. Usually your Activity object. If
+ * context is not an instance of Activity (or a subclass of it),
+ * crashes will be sent automatically.
+ * @param listener Implement this for callback functions.
+ * @return The configured crash manager for method chaining.
+ */
+ public Crashes register(Context context, CrashesListener listener) {
+ return register(context, Constants.BASE_URL, listener);
+ }
+
+ /**
+ * Registers the crash manager and handles existing crash logs.
+ * AvalancheHub app identifier is read from configuration values in manifest.
+ *
+ * @param context The context to use. Usually your Activity object. If
+ * context is not an instance of Activity (or a subclass of it),
+ * crashes will be sent automatically.
+ * @param endpointUrl URL of the AvalancheHub endpoint to use.
+ * @param listener Implement this for callback functions.
+ * @return The configured crash manager for method chaining.
+ */
+ public Crashes register(Context context, String endpointUrl, CrashesListener listener) {
+ return register(context, endpointUrl, listener, false);
+ }
+
+ /**
+ * Registers the crash manager and handles existing crash logs.
+ * AvalancheHub app identifier is read from configuration values in manifest.
+ *
+ * @param context The context to use. Usually your Activity object. If
+ * context is not an instance of Activity (or a subclass of it),
+ * crashes will be sent automatically.
+ * @param endpointUrl URL of the AvalancheHub endpoint to use.
+ * @param listener Implement this for callback functions.
+ * @param lazyExecution Whether the manager should execute lazily, e.g. not check for crashes right away.
+ * @return
+ */
+ public Crashes register(Context context, String endpointUrl, CrashesListener listener, boolean lazyExecution) {
+ mContextWeakReference = new WeakReference<>(context);
+ mListener = listener;
+ mEndpointUrl = endpointUrl;
+ mLazyExecution = lazyExecution;
+
+ initialize();
+
+ return this;
+ }
+
+ private void initialize() {
+ Context context = mContextWeakReference.get();
+
+ if (context != null) {
+ if (mInitializeTimestamp == 0) {
+ mInitializeTimestamp = System.currentTimeMillis();
+ }
+
+ Constants.loadFromContext(context);
+
+ if (!mLazyExecution) {
+ execute();
+ }
+ }
+ }
+
+ /**
+ * Allows you to execute the crash manager later on-demand.
+ */
+ public void execute() {
+ Context context = mContextWeakReference.get();
+ if (context == null) {
+ return;
+ }
+
+ int foundOrSend = hasStackTraces(context);
+
+ if (foundOrSend == STACK_TRACES_FOUND_NEW) {
+ mDidCrashInLastSession = true;
+ Boolean autoSend = !(context instanceof Activity);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ autoSend |= prefs.getBoolean(ALWAYS_SEND_KEY, false);
+
+ if (mListener != null) {
+ autoSend |= mListener.shouldAutoUploadCrashes();
+ autoSend |= mListener.onCrashesFound();
+
+ mListener.onNewCrashesFound();
+ }
+
+ if (!autoSend) {
+ showDialog(mContextWeakReference);
+ } else {
+ sendCrashes();
+ }
+ } else if (foundOrSend == STACK_TRACES_FOUND_CONFIRMED) {
+ if (mListener != null) {
+ mListener.onConfirmedCrashesFound();
+ }
+
+ sendCrashes();
+ } else {
+ registerExceptionHandler();
+ }
+ }
+
+ private void registerExceptionHandler() {
+ if (!isEmpty(Constants.APP_VERSION) && !isEmpty(Constants.APP_PACKAGE)) {
+ // Get current handler
+ Thread.UncaughtExceptionHandler currentHandler = Thread.getDefaultUncaughtExceptionHandler();
+ if (currentHandler != null) {
+ AvalancheLog.debug("Current handler class = " + currentHandler.getClass().getName());
+ }
+
+ // Update listener if already registered, otherwise set new handler
+ if (currentHandler instanceof ExceptionHandler) {
+ ((ExceptionHandler) currentHandler).setListener(mListener);
+ } else {
+ Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler(this, currentHandler, mListener, isIgnoreDefaultHandler()));
+ }
+ } else {
+ AvalancheLog.warn("Exception handler not set because app version or package is null.");
+ }
+ }
+
+ private void sendCrashes() {
+ sendCrashes(null);
+ }
+
+ private void sendCrashes(final CrashMetaData crashMetaData) {
+ saveConfirmedStackTraces();
+ registerExceptionHandler();
+
+ Context context = mContextWeakReference.get();
+ if (context != null && !Util.isConnectedToNetwork(context)) {
+ // Not connected to network, not trying to submit stack traces
+ return;
+ }
+
+ if (!mIsSubmitting) {
+ mIsSubmitting = true;
+
+ new Thread() {
+ @Override
+ public void run() {
+ submitStackTraces(crashMetaData);
+ mIsSubmitting = false;
+ }
+ }.start();
+ }
+ }
+
+ /**
+ * Submits all stack traces in the files dir to AvalancheHub.
+ *
+ * @param crashMetaData The crashMetaData, provided by the user.
+ */
+ public void submitStackTraces(CrashMetaData crashMetaData) {
+ String[] list = searchForStackTraces();
+ Boolean successful = false;
+
+ Context context = mContextWeakReference.get();
+
+ if ((list != null) && (list.length > 0)) {
+ AvalancheLog.debug("Found " + list.length + " stacktrace(s).");
+
+ for (int index = 0; index < list.length; index++) {
+ HttpURLConnection urlConnection = null;
+ try {
+ // Read contents of stack trace
+ String filename = list[index];
+ String stacktrace = Util.contentsOfFile(context, filename);
+ if (stacktrace.length() > 0) {
+ // Transmit stack trace with POST request
+
+ AvalancheLog.debug("Transmitting crash data: \n" + stacktrace);
+
+ // Retrieve user ID and contact information if given
+ String userID = Util.contentsOfFile(context, filename.replace(".stacktrace", ".user"));
+ String contact = Util.contentsOfFile(context, filename.replace(".stacktrace", ".contact"));
+
+ if (crashMetaData != null) {
+ final String crashMetaDataUserID = crashMetaData.getUserID();
+ if (!isEmpty(crashMetaDataUserID)) {
+ userID = crashMetaDataUserID;
+ }
+ final String crashMetaDataContact = crashMetaData.getUserEmail();
+ if (!isEmpty(crashMetaDataContact)) {
+ contact = crashMetaDataContact;
+ }
+ }
+
+ // Append application log to user provided description if present, if not, just send application log
+ final String applicationLog = Util.contentsOfFile(context, filename.replace(".stacktrace", ".description"));
+ String description = crashMetaData != null ? crashMetaData.getUserDescription() : "";
+ if (!isEmpty(applicationLog)) {
+ if (!isEmpty(description)) {
+ description = String.format("%s\n\nLog:\n%s", description, applicationLog);
+ } else {
+ description = String.format("Log:\n%s", applicationLog);
+ }
+ }
+
+ Map parameters = new HashMap();
+
+ parameters.put("raw", stacktrace);
+ parameters.put("userID", userID);
+ parameters.put("contact", contact);
+ parameters.put("description", description);
+ parameters.put("sdk", Constants.SDK_NAME);
+ parameters.put("sdk_version", BuildConfig.VERSION_NAME);
+
+ urlConnection = new HttpURLConnectionBuilder(getURLString())
+ .setRequestMethod("POST")
+ .writeFormFields(parameters)
+ .build();
+
+ int responseCode = urlConnection.getResponseCode();
+
+ successful = (responseCode == HttpURLConnection.HTTP_ACCEPTED || responseCode == HttpURLConnection.HTTP_CREATED);
+
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ if (urlConnection != null) {
+ urlConnection.disconnect();
+ }
+ if (successful) {
+ AvalancheLog.debug("Transmission succeeded");
+ deleteStackTrace(list[index]);
+
+ if (mListener != null) {
+ mListener.onCrashesSent();
+ deleteRetryCounter(list[index], mListener.getMaxRetryAttempts());
+ }
+ } else {
+ AvalancheLog.debug("Transmission failed, will retry on next register() call");
+ if (mListener != null) {
+ mListener.onCrashesNotSent();
+ updateRetryCounter(list[index], mListener.getMaxRetryAttempts());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows a dialog to ask the user whether he wants to send crash reports to
+ * AvalancheHub or delete them.
+ */
+ private void showDialog(final WeakReference weakContext) {
+ Context context = null;
+ if (weakContext != null) {
+ context = weakContext.get();
+ }
+
+ if (context == null) {
+ return;
+ }
+
+ if (mListener != null && mListener.onHandleAlertView()) {
+ return;
+ }
+
+ final boolean ignoreDefaultHandler = isIgnoreDefaultHandler();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ String alertTitle = getAlertTitle(context);
+ builder.setTitle(alertTitle);
+ builder.setMessage(R.string.avalanche_crash_dialog_message);
+
+ builder.setNegativeButton(R.string.avalanche_crash_dialog_negative_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ handleUserInput(CrashesUserInput.CrashManagerUserInputDontSend, null, mListener, weakContext, ignoreDefaultHandler);
+ }
+ });
+
+ builder.setNeutralButton(R.string.avalanche_crash_dialog_neutral_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ handleUserInput(CrashesUserInput.CrashManagerUserInputAlwaysSend, null, mListener, weakContext, ignoreDefaultHandler);
+ }
+ });
+
+ builder.setPositiveButton(R.string.avalanche_crash_dialog_positive_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ handleUserInput(CrashesUserInput.CrashManagerUserInputSend, null, mListener,
+ weakContext, ignoreDefaultHandler);
+ }
+ });
+
+ builder.create().show();
+ }
+
+ /**
+ * Provides an interface to pass user input from a custom alert to a crash report
+ *
+ * @param userInput Defines the users action whether to send, always send, or not to send the crash report.
+ * @param userProvidedMetaData The content of this optional CrashMetaData instance will be attached to the crash report
+ * and allows to ask the user for e.g. additional comments or info.
+ * @param listener an optional crash manager listener to use.
+ * @param weakContext The context to use. Usually your Activity object.
+ * @param ignoreDefaultHandler whether to ignore the default exception handler.
+ * @return true if the input is a valid option and successfully triggered further processing of the crash report.
+ * @see CrashesUserInput
+ * @see CrashMetaData
+ * @see CrashesListener
+ */
+ public boolean handleUserInput(final CrashesUserInput userInput,
+ final CrashMetaData userProvidedMetaData, final CrashesListener listener,
+ final WeakReference weakContext, final boolean ignoreDefaultHandler) {
+ switch (userInput) {
+ case CrashManagerUserInputDontSend:
+ if (listener != null) {
+ listener.onUserDeniedCrashes();
+ }
+
+ deleteStackTraces();
+ registerExceptionHandler();
+ return true;
+ case CrashManagerUserInputAlwaysSend:
+ Context context = null;
+ if (weakContext != null) {
+ context = weakContext.get();
+ }
+
+ if (context == null) {
+ return false;
+ }
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.edit().putBoolean(ALWAYS_SEND_KEY, true).apply();
+
+ sendCrashes(userProvidedMetaData);
+ return true;
+ case CrashManagerUserInputSend:
+ sendCrashes(userProvidedMetaData);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ long getInitializeTimestamp() {
+ return mInitializeTimestamp;
+ }
+
+ /**
+ * Checks if there are any saved stack traces in the files dir.
+ *
+ * @param context The context to use. Usually your Activity object.
+ * @return STACK_TRACES_FOUND_NONE if there are no stack traces,
+ * STACK_TRACES_FOUND_NEW if there are any new stack traces,
+ * STACK_TRACES_FOUND_CONFIRMED if there only are confirmed stack traces.
+ */
+ public int hasStackTraces(Context context) {
+ String[] filenames = searchForStackTraces();
+ List confirmedFilenames = null;
+ int result = STACK_TRACES_FOUND_NONE;
+ if ((filenames != null) && (filenames.length > 0)) {
+ try {
+ confirmedFilenames = getConfirmedFilenames(context);
+
+ } catch (Exception e) {
+ // Just in case, we catch all exceptions here
+ }
+
+ if (confirmedFilenames != null) {
+ result = STACK_TRACES_FOUND_CONFIRMED;
+
+ for (String filename : filenames) {
+ if (!confirmedFilenames.contains(filename)) {
+ result = STACK_TRACES_FOUND_NEW;
+ break;
+ }
+ }
+ } else {
+ result = STACK_TRACES_FOUND_NEW;
+ }
+ }
+
+ return result;
+ }
+
+ public boolean didCrashInLastSession() {
+ return mDidCrashInLastSession;
+ }
+
+ public CrashReport getLastCrashDetails() {
+ if (Constants.FILES_PATH == null || !didCrashInLastSession()) {
+ return null;
+ }
+
+ File dir = new File(Constants.FILES_PATH + "/");
+ File[] files = dir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String filename) {
+ return filename.endsWith(".stacktrace");
+ }
+ });
+
+ long lastModification = 0;
+ File lastModifiedFile = null;
+ CrashReport result = null;
+ for (File file : files) {
+ if (file.lastModified() > lastModification) {
+ lastModification = file.lastModified();
+ lastModifiedFile = file;
+ }
+ }
+
+ if (lastModifiedFile != null && lastModifiedFile.exists()) {
+ try {
+ result = CrashReport.fromFile(lastModifiedFile);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return result;
+ }
+
+ private void saveConfirmedStackTraces() {
+ Context context = mContextWeakReference.get();
+ if (context != null) {
+ try {
+ String[] filenames = searchForStackTraces();
+ SharedPreferences preferences = context.getSharedPreferences("AvalancheSDK", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString("ConfirmedFilenames", Util.joinArray(filenames, "|"));
+ editor.apply();
+ } catch (Exception e) {
+ // Just in case, we catch all exceptions here
+ }
+ }
+ }
+
+ public void deleteStackTraces() {
+ String[] list = searchForStackTraces();
+
+ if ((list != null) && (list.length > 0)) {
+ AvalancheLog.debug("Found " + list.length + " stacktrace(s).");
+
+ for (int index = 0; index < list.length; index++) {
+ try {
+ Context context = mContextWeakReference.get();
+ AvalancheLog.debug("Delete stacktrace " + list[index] + ".");
+ deleteStackTrace(list[index]);
+
+ if (context != null) {
+ context.deleteFile(list[index]);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Deletes the given filename and all corresponding files (same name,
+ * different extension).
+ */
+ private void deleteStackTrace(String filename) {
+ Context context = mContextWeakReference.get();
+ context.deleteFile(filename);
+
+ String user = filename.replace(".stacktrace", ".user");
+ context.deleteFile(user);
+
+ String contact = filename.replace(".stacktrace", ".contact");
+ context.deleteFile(contact);
+
+ String description = filename.replace(".stacktrace", ".description");
+ context.deleteFile(description);
+ }
+
+ /**
+ * Update the retry attempts count for this crash stacktrace.
+ */
+ private void updateRetryCounter(String filename, int maxRetryAttempts) {
+ if (maxRetryAttempts == -1) {
+ return;
+ }
+
+ Context context = mContextWeakReference.get();
+ SharedPreferences preferences = context.getSharedPreferences("AvalancheSDK", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+
+ int retryCounter = preferences.getInt("RETRY_COUNT: " + filename, 0);
+ if (retryCounter >= maxRetryAttempts) {
+ deleteStackTrace(filename);
+ deleteRetryCounter(filename, maxRetryAttempts);
+ } else {
+ editor.putInt("RETRY_COUNT: " + filename, retryCounter + 1);
+ editor.apply();
+ }
+ }
+
+ /**
+ * Delete the retry counter if stacktrace is uploaded or retry limit is
+ * reached.
+ */
+ private void deleteRetryCounter(String filename, int maxRetryAttempts) {
+ Context context = mContextWeakReference.get();
+ SharedPreferences preferences = context.getSharedPreferences("AvalancheSDK", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.remove("RETRY_COUNT: " + filename);
+ editor.apply();
+ }
+
+ private boolean isIgnoreDefaultHandler() {
+ return mListener != null && mListener.ignoreDefaultHandler();
+ }
+
+ private String getURLString() {
+ return mEndpointUrl + "api/2/apps/" + AvalancheHub.getSharedInstance().getAppIdentifier() + "/crashes/";
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/CrashesListener.java b/crash/src/main/java/avalanche/crash/CrashesListener.java
new file mode 100644
index 0000000000..917b770e00
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/CrashesListener.java
@@ -0,0 +1,155 @@
+package avalanche.crash;
+
+/**
+ *
Description
+ *
+ * Abstract class for callbacks to be invoked from the Crashes.
+ *
+ **/
+public abstract class CrashesListener {
+ /**
+ * Return true to ignore the default exception handler, i.e. the user will not
+ * get the alert dialog with the "Force Close" button.
+ *
+ * @return if true, the default handler is ignored
+ */
+ public boolean ignoreDefaultHandler() {
+ return false;
+ }
+
+ /**
+ * Return false to remove the device data (OS version, manufacturer, model)
+ * from the crash log, e.g. if some of your testers are using unreleased
+ * devices.
+ *
+ * @return if true, the crash report will include device data
+ */
+ public boolean includeDeviceData() {
+ return true;
+ }
+
+ /**
+ * Return false to remove the stable device identifier from the
+ * crash log, e.g. if there are privacy concerns.
+ *
+ * @return if true, the crash report will include a stable device identifier
+ */
+ public boolean includeDeviceIdentifier() {
+ return true;
+ }
+
+ /**
+ * Return true to include information about the crashed thread if available.
+ *
+ * @return if true, the crash report will include thread id and name if available
+ */
+ public boolean includeThreadDetails() {
+ return true;
+ }
+
+ /**
+ * Return contact data or similar; note that this has privacy implications,
+ * so you might want to return nil for release builds! The string will be
+ * limited to 255 characters.
+ *
+ * @return the contact string
+ */
+ public String getContact() {
+ return null;
+ }
+
+ /**
+ * Return additional data, i.e. parts of the system log, the last server
+ * response or similar. This string is not limited to a certain size.
+ *
+ * @return a description
+ */
+ public String getDescription() {
+ return null;
+ }
+
+ /**
+ * Return a user ID or similar; note that this has privacy implications,
+ * so you might want to return nil for release builds! The string will be
+ * limited to 255 characters.
+ *
+ * @return the user's ID
+ */
+ public String getUserID() {
+ return null;
+ }
+
+ /**
+ * Called when the crash manager found one or more crashes. Return true
+ * if you want to auto-send crashes (i.e. not ask the user)
+ *
+ * @return if true, crashes are sent automatically
+ * @deprecated Replace this method with onNewCrashesFound,
+ * onConfirmedCrashesFound, and shouldAutoUploadCrashReport.
+ */
+ public boolean onCrashesFound() {
+ return false;
+ }
+
+ /**
+ * Return true if you want to auto-send crashes. Note that this method
+ * is only called if new crashes were found.
+ *
+ * @return if true, crashes are sent automatically
+ */
+ public boolean shouldAutoUploadCrashes() {
+ return false;
+ }
+
+ /**
+ * Called when the crash manager has found new crash logs.
+ */
+ public void onNewCrashesFound() {
+ }
+
+ /**
+ * Called when the crash manager has found crash logs that were already
+ * confirmed by the user or should have been auto uploaded, but the upload
+ * failed, e.g. in case of a network failure.
+ */
+ public void onConfirmedCrashesFound() {
+ }
+
+ /**
+ * Called when the crash manager has sent crashes to AvalancheHub.
+ */
+ public void onCrashesSent() {
+ }
+
+ /**
+ * Called when the crash manager failed to send crashes to AvalancheHub, e.g.
+ * because the device has no network connections.
+ */
+ public void onCrashesNotSent() {
+ }
+
+ /**
+ * Called when the user denied to send crashes to AvalancheHub.
+ */
+ public void onUserDeniedCrashes() {
+ }
+
+ /**
+ * Get the number of max retry attempts to send crashes to AvalancheHub.
+ * Infinite retries if this value is set to -1
+ *
+ * @return the max number of retry attempts
+ */
+ public int getMaxRetryAttempts() {
+ return 1;
+ }
+
+ /**
+ * Called when dialog should be displayed to inform the user about crash.
+ *
+ * @return if true, alert-view is handled by user
+ */
+ public boolean onHandleAlertView() {
+ return false;
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/ExceptionHandler.java b/crash/src/main/java/avalanche/crash/ExceptionHandler.java
new file mode 100644
index 0000000000..bcdcfe9102
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/ExceptionHandler.java
@@ -0,0 +1,227 @@
+package avalanche.crash;
+
+import android.text.TextUtils;
+import avalanche.base.Constants;
+import avalanche.base.Channel;
+import avalanche.crash.model.CrashReport;
+import avalanche.base.utils.AvalancheLog;
+
+import java.io.*;
+import java.lang.Thread.UncaughtExceptionHandler;
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ *
Description
+ * Helper class to catch exceptions. Saves the stack trace
+ * as a file and executes callback methods to ask the app for
+ * additional information and meta data (see CrashesListener).
+ *
+ **/
+public class ExceptionHandler implements UncaughtExceptionHandler {
+ private final Crashes mCrashes;
+ private boolean mIgnoreDefaultHandler = false;
+ private CrashesListener mCrashesListener;
+ private UncaughtExceptionHandler mDefaultExceptionHandler;
+
+ public ExceptionHandler(Crashes crashes, UncaughtExceptionHandler defaultExceptionHandler, CrashesListener listener, boolean ignoreDefaultHandler) {
+ mCrashes = crashes;
+ mDefaultExceptionHandler = defaultExceptionHandler;
+ mIgnoreDefaultHandler = ignoreDefaultHandler;
+ mCrashesListener = listener;
+ }
+
+ public void setListener(CrashesListener listener) {
+ mCrashesListener = listener;
+ }
+
+
+
+ /**
+ * Save a caught exception to disk.
+ *
+ * @param exception Exception to save.
+ * @param thread Thread that crashed.
+ * @param listener Custom Crashes listener instance.
+ */
+ public void saveException(Throwable exception, Thread thread, CrashesListener listener) {
+ final Date now = new Date();
+ final Date startDate = new Date(mCrashes.getInitializeTimestamp());
+ final Writer result = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(result);
+ exception.printStackTrace(printWriter);
+
+ String filename = UUID.randomUUID().toString();
+
+ CrashReport crashReport = new CrashReport(filename, exception);
+ crashReport.setAppPackage(Constants.APP_PACKAGE);
+ crashReport.setAppVersionCode(Constants.APP_VERSION);
+ crashReport.setAppVersionName(Constants.APP_VERSION_NAME);
+ crashReport.setAppStartDate(startDate);
+ crashReport.setAppCrashDate(now);
+
+ if ((listener == null) || (listener.includeDeviceData())) {
+ crashReport.setOsVersion(Constants.ANDROID_VERSION);
+ crashReport.setOsBuild(Constants.ANDROID_BUILD);
+ crashReport.setDeviceManufacturer(Constants.PHONE_MANUFACTURER);
+ crashReport.setDeviceModel(Constants.PHONE_MODEL);
+ }
+
+ if (thread != null && ((listener == null) || (listener.includeThreadDetails()))) {
+ crashReport.setThreadName(thread.getName() + "-" + thread.getId());
+ }
+
+ if (Constants.CRASH_IDENTIFIER != null && (listener == null || listener.includeDeviceIdentifier())) {
+ crashReport.setReporterKey(Constants.CRASH_IDENTIFIER);
+ }
+
+ crashReport.writeCrashReport();
+
+ if (listener != null) {
+ try {
+ writeValueToFile(limitedString(listener.getUserID()), filename + ".user");
+ writeValueToFile(limitedString(listener.getContact()), filename + ".contact");
+ writeValueToFile(listener.getDescription(), filename + ".description");
+ } catch (IOException e) {
+ AvalancheLog.error("Error saving crash meta data!", e);
+ }
+
+ }
+ }
+
+ /**
+ * Save java exception(s) caught by XamarinSDK to disk.
+ *
+ * @param exception The native java exception to save.
+ * @param managedExceptionString String representation of the full exception including the managed exception.
+ * @param thread Thread that crashed.
+ * @param listener Custom Crashes listener instance.
+ */
+ @SuppressWarnings("unused")
+ public void saveNativeException(Throwable exception, String managedExceptionString, Thread thread, CrashesListener listener) {
+ // the throwable will a "native" Java exception. In this case managedExceptionString contains the full, "unconverted" exception
+ // which contains information about the managed exception, too. We don't want to loose that part. Sadly, passing a managed
+ // exception as an additional throwable strips that info, so we pass in the full managed exception as a string
+ // and extract the first part that contains the info about the managed code that was calling the java code.
+ // In case there is no managedExceptionString, we just forward the java exception
+ if (!TextUtils.isEmpty(managedExceptionString)) {
+ String[] splits = managedExceptionString.split("--- End of managed exception stack trace ---", 2);
+ if (splits != null && splits.length > 0) {
+ managedExceptionString = splits[0];
+ }
+ }
+
+ saveXamarinException(exception, thread, managedExceptionString, false, listener);
+ }
+
+ /**
+ * Save managed exception(s) caught by XamarinSDK to disk.
+ *
+ * @param exception The managed exception to save.
+ * @param thread Thread that crashed.
+ * @param listener Custom Crashes listener instance.
+ */
+ @SuppressWarnings("unused")
+ public void saveManagedException(Throwable exception, Thread thread, CrashesListener listener) {
+ saveXamarinException(exception, thread, null, true, listener);
+ }
+
+ //TODO refacture so we don't have duplicate code
+ private void saveXamarinException(Throwable exception, Thread thread, String additionalManagedException, Boolean isManagedException, CrashesListener listener) {
+ final Date startDate = new Date(mCrashes.getInitializeTimestamp());
+ String filename = UUID.randomUUID().toString();
+ final Date now = new Date();
+
+ final Writer result = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(result);
+ if (exception != null) {
+ exception.printStackTrace(printWriter);
+ }
+
+
+ //TODO move this to a Factory class
+ CrashReport crashReport = new CrashReport(filename, exception, additionalManagedException, isManagedException);
+ crashReport.setAppPackage(Constants.APP_PACKAGE);
+ crashReport.setAppVersionCode(Constants.APP_VERSION);
+ crashReport.setAppVersionName(Constants.APP_VERSION_NAME);
+ crashReport.setAppStartDate(startDate);
+ crashReport.setAppCrashDate(now);
+
+ if ((listener == null) || (listener.includeDeviceData())) {
+ crashReport.setOsVersion(Constants.ANDROID_VERSION);
+ crashReport.setOsBuild(Constants.ANDROID_BUILD);
+ crashReport.setDeviceManufacturer(Constants.PHONE_MANUFACTURER);
+ crashReport.setDeviceModel(Constants.PHONE_MODEL);
+ }
+
+ if (thread != null && ((listener == null) || (listener.includeThreadDetails()))) {
+ crashReport.setThreadName(thread.getName() + "-" + thread.getId());
+ }
+
+ if (Constants.CRASH_IDENTIFIER != null && (listener == null || listener.includeDeviceIdentifier())) {
+ crashReport.setReporterKey(Constants.CRASH_IDENTIFIER);
+ }
+
+ crashReport.writeCrashReport();
+
+ Channel.getInstance().handle(crashReport);
+
+ if (listener != null) {
+ try {
+ writeValueToFile(limitedString(listener.getUserID()), filename + ".user");
+ writeValueToFile(limitedString(listener.getContact()), filename + ".contact");
+ writeValueToFile(listener.getDescription(), filename + ".description");
+ } catch (IOException e) {
+ AvalancheLog.error("Error saving crash meta data!", e);
+ }
+
+ }
+ }
+
+ //TODO: this should be the only method here, no persisting logic in here.
+ public void uncaughtException(Thread thread, Throwable exception) {
+ if (Constants.FILES_PATH == null) {
+ // If the files path is null, the exception can't be stored
+ // Always call the default handler instead
+ mDefaultExceptionHandler.uncaughtException(thread, exception);
+ } else {
+ saveException(exception, thread, mCrashesListener);
+
+ if (!mIgnoreDefaultHandler) {
+ mDefaultExceptionHandler.uncaughtException(thread, exception);
+ } else {
+ android.os.Process.killProcess(android.os.Process.myPid());
+ System.exit(10);
+ }
+ }
+ }
+
+ //TODO: this should be part of the pipleine in the base module
+ private static void writeValueToFile(String value, String filename) throws IOException {
+ if (TextUtils.isEmpty(value)) {
+ return;
+ }
+ BufferedWriter writer = null;
+ try {
+ String path = Constants.FILES_PATH + "/" + filename;
+ if (!TextUtils.isEmpty(value) && TextUtils.getTrimmedLength(value) > 0) {
+ writer = new BufferedWriter(new FileWriter(path));
+ writer.write(value);
+ writer.flush();
+ }
+ } catch (IOException e) {
+ // TODO: Handle exception here
+ } finally {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+ }
+
+ private static String limitedString(String string) {
+ if (!TextUtils.isEmpty(string) && string.length() > 255) {
+ string = string.substring(0, 255);
+ }
+ return string;
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/ingestion/models/Binary.java b/crash/src/main/java/avalanche/crash/ingestion/models/Binary.java
new file mode 100755
index 0000000000..f4fa6adccc
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/ingestion/models/Binary.java
@@ -0,0 +1,82 @@
+package avalanche.crash.ingestion.models;
+
+public class Binary {
+
+ private String startAddress = null;
+
+ private String endAddress = null;
+
+ private String name = null;
+
+ private String cpuType = null;
+
+ private String cpuSubType = null;
+
+ private String uuid = null;
+
+ private String path = null;
+
+
+ public String getStartAddress() {
+ return startAddress;
+ }
+
+ public void setStartAddress(String startAddress) {
+ this.startAddress = startAddress;
+ }
+
+
+ public String getEndAddress() {
+ return endAddress;
+ }
+
+ public void setEndAddress(String endAddress) {
+ this.endAddress = endAddress;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+
+ public String getCpuType() {
+ return cpuType;
+ }
+
+ public void setCpuType(String cpuType) {
+ this.cpuType = cpuType;
+ }
+
+
+ public String getCpuSubType() {
+ return cpuSubType;
+ }
+
+ public void setCpuSubType(String cpuSubType) {
+ this.cpuSubType = cpuSubType;
+ }
+
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ /**
+ **/
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+}
diff --git a/crash/src/main/java/avalanche/crash/ingestion/models/CrashLog.java b/crash/src/main/java/avalanche/crash/ingestion/models/CrashLog.java
new file mode 100755
index 0000000000..8edc01044d
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/ingestion/models/CrashLog.java
@@ -0,0 +1,165 @@
+package avalanche.crash.ingestion.models;
+
+import avalanche.base.ingestion.models.InSessionLog;
+
+import java.util.List;
+import java.util.Map;
+
+
+public class CrashLog extends InSessionLog {
+
+ private String type = null;
+ private Map properties = null;
+ private String sid = null;
+ private String id = null;
+ private String process = null;
+ private Integer processId = null;
+ private String parentProcess = null;
+ private Integer parentProcessId = null;
+ private Integer crashThread = null;
+ private String applicationPath = null;
+ private String exceptionType = null;
+ private String exceptionCode = null;
+ private String exceptionAddress = null;
+ private String exceptionReason = null;
+ private List threads = null;
+ private List binaries = null;
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public Map getProperties() {
+ return properties;
+ }
+
+ public void setProperties(Map properties) {
+ this.properties = properties;
+ }
+
+
+ public String getSid() {
+ return sid;
+ }
+
+ public void setSid(String sid) {
+ this.sid = sid;
+ }
+
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+
+ public String getProcess() {
+ return process;
+ }
+
+ public void setProcess(String process) {
+ this.process = process;
+ }
+
+ public Integer getProcessId() {
+ return processId;
+ }
+
+ public void setProcessId(Integer processId) {
+ this.processId = processId;
+ }
+
+ public String getParentProcess() {
+ return parentProcess;
+ }
+
+ public void setParentProcess(String parentProcess) {
+ this.parentProcess = parentProcess;
+ }
+
+ public Integer getParentProcessId() {
+ return parentProcessId;
+ }
+
+ public void setParentProcessId(Integer parentProcessId) {
+ this.parentProcessId = parentProcessId;
+ }
+
+ public Integer getCrashThread() {
+ return crashThread;
+ }
+
+ public void setCrashThread(Integer crashThread) {
+ this.crashThread = crashThread;
+ }
+
+
+ public String getApplicationPath() {
+ return applicationPath;
+ }
+
+ public void setApplicationPath(String applicationPath) {
+ this.applicationPath = applicationPath;
+ }
+
+
+ public String getExceptionType() {
+ return exceptionType;
+ }
+
+ public void setExceptionType(String exceptionType) {
+ this.exceptionType = exceptionType;
+ }
+
+
+ public String getExceptionCode() {
+ return exceptionCode;
+ }
+
+ public void setExceptionCode(String exceptionCode) {
+ this.exceptionCode = exceptionCode;
+ }
+
+
+ public String getExceptionAddress() {
+ return exceptionAddress;
+ }
+
+ public void setExceptionAddress(String exceptionAddress) {
+ this.exceptionAddress = exceptionAddress;
+ }
+
+
+ public String getExceptionReason() {
+ return exceptionReason;
+ }
+
+ public void setExceptionReason(String exceptionReason) {
+ this.exceptionReason = exceptionReason;
+ }
+
+
+ public List getThreads() {
+ return threads;
+ }
+
+ public void setThreads(List threads) {
+ this.threads = threads;
+ }
+
+
+ public List getBinaries() {
+ return binaries;
+ }
+
+ public void setBinaries(List binaries) {
+ this.binaries = binaries;
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/ingestion/models/Thread.java b/crash/src/main/java/avalanche/crash/ingestion/models/Thread.java
new file mode 100755
index 0000000000..c40826a4b3
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/ingestion/models/Thread.java
@@ -0,0 +1,27 @@
+package avalanche.crash.ingestion.models;
+
+import java.util.List;
+
+
+public class Thread {
+
+ private Integer id = null;
+ private List frames = null;
+
+
+ public Integer getId() {
+ return id;
+ }
+
+ public void setId(Integer id) {
+ this.id = id;
+ }
+
+ public List getFrames() {
+ return frames;
+ }
+
+ public void setFrames(List frames) {
+ this.frames = frames;
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/ingestion/models/ThreadFrame.java b/crash/src/main/java/avalanche/crash/ingestion/models/ThreadFrame.java
new file mode 100755
index 0000000000..fd34b953eb
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/ingestion/models/ThreadFrame.java
@@ -0,0 +1,39 @@
+package avalanche.crash.ingestion.models;
+
+
+public class ThreadFrame {
+
+ private String address = null;
+ private String symbol = null;
+
+ public String getAddress() {
+ return address;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ }
+
+ public String getSymbol() {
+ return symbol;
+ }
+
+ public void setSymbol(String symbol) {
+ this.symbol = symbol;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ThreadFrame threadFrame = (ThreadFrame) o;
+ return (address == null ? threadFrame.address == null : address.equals(threadFrame.address)) &&
+ (symbol == null ? threadFrame.symbol == null : symbol.equals(threadFrame.symbol));
+ }
+
+}
diff --git a/crash/src/main/java/avalanche/crash/model/CrashMetaData.java b/crash/src/main/java/avalanche/crash/model/CrashMetaData.java
new file mode 100644
index 0000000000..0fb1147294
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/model/CrashMetaData.java
@@ -0,0 +1,44 @@
+package avalanche.crash.model;
+
+/**
+ * This class provides properties that can be attached to a crash report via a custom alert view flow
+ *
+ */
+public class CrashMetaData {
+ private String mUserDescription;
+ private String mUserEmail;
+ private String mUserID;
+
+ public String getUserDescription() {
+ return mUserDescription;
+ }
+
+ public void setUserDescription(final String userDescription) {
+ this.mUserDescription = userDescription;
+ }
+
+ public String getUserEmail() {
+ return mUserEmail;
+ }
+
+ public void setUserEmail(final String userEmail) {
+ this.mUserEmail = userEmail;
+ }
+
+ public String getUserID() {
+ return mUserID;
+ }
+
+ public void setUserID(final String userID) {
+ this.mUserID = userID;
+ }
+
+ @Override
+ public String toString() {
+ return "\n" + CrashMetaData.class.getSimpleName()
+ + "\n" + "userDescription " + mUserDescription
+ + "\n" + "userEmail " + mUserEmail
+ + "\n" + "userID " + mUserID
+ ;
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/model/CrashReport.java b/crash/src/main/java/avalanche/crash/model/CrashReport.java
new file mode 100644
index 0000000000..1ec385060f
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/model/CrashReport.java
@@ -0,0 +1,368 @@
+package avalanche.crash.model;
+
+import android.text.TextUtils;
+
+import avalanche.base.AvalancheDataInterface;
+import avalanche.base.Constants;
+import avalanche.base.utils.AvalancheLog;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class CrashReport implements AvalancheDataInterface {
+
+ public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy", Locale.US);
+
+ private static final String FIELD_CRASH_REPORTER_KEY = "CrashReporter Key";
+ private static final String FIELD_APP_START_DATE = "Start Date";
+ private static final String FIELD_APP_CRASH_DATE = "Date";
+ private static final String FIELD_OS_VERSION = "Android";
+ private static final String FIELD_OS_BUILD = "Android Build";
+ private static final String FIELD_DEVICE_MANUFACTURER = "Manufacturer";
+ private static final String FIELD_DEVICE_MODEL = "Model";
+ private static final String FIELD_APP_PACKAGE = "Package";
+ private static final String FIELD_APP_VERSION_NAME = "Version Name";
+ private static final String FIELD_APP_VERSION_CODE = "Version Code";
+ private static final String FIELD_THREAD_NAME = "Thread";
+
+ private static final String FIELD_FORMAT = "Format";
+ private static final String FIELD_FORMAT_VALUE = "Xamarin";
+ private static final String FIELD_XAMARIN_CAUSED_BY = "Xamarin caused by: "; //Field that marks a Xamarin Exception
+
+ private final String crashIdentifier;
+
+ private String reporterKey;
+
+ private Date appStartDate;
+ private Date appCrashDate;
+
+ private String osVersion;
+ private String osBuild;
+ private String deviceManufacturer;
+ private String deviceModel;
+
+ private String appPackage;
+ private String appVersionName;
+ private String appVersionCode;
+
+ private String threadName;
+
+ private String throwableStackTrace;
+
+ private Boolean isXamarinException;
+
+ private String format;
+
+ public CrashReport(String crashIdentifier) {
+ this.crashIdentifier = crashIdentifier;
+ }
+
+ public CrashReport(String crashIdentifier, Throwable throwable) {
+ this(crashIdentifier);
+
+ isXamarinException = false;
+
+ final Writer stackTraceResult = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(stackTraceResult);
+ throwable.printStackTrace(printWriter);
+ throwableStackTrace = stackTraceResult.toString();
+ }
+
+ public CrashReport(String crashIdentifier, Throwable throwable, String managedExceptionString, Boolean isManagedException) {
+ this(crashIdentifier);
+
+ final Writer stackTraceResult = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(stackTraceResult);
+
+ isXamarinException = true;
+
+ //Add the header field "Format" to the crash
+ //the value is "Xamarin", for now there are no other values and it's only set in case we have an exception coming from
+ //the Xamarin SDK. It can be a java exception, a managed exception, or a mixed one.
+ setFormat(FIELD_FORMAT_VALUE);
+
+ if (isManagedException) {
+ //add "Xamarin Caused By" before the managed stacktrace. No new line after it.
+ printWriter.print(FIELD_XAMARIN_CAUSED_BY);
+
+ //print the managed exception
+ throwable.printStackTrace(printWriter);
+ } else {
+ //If we have managedExceptionString, we hava a MIXED (Java & C#)
+ //exception, The throwable will be the Java exception.
+ if (!TextUtils.isEmpty(managedExceptionString)) {
+ //Print the java exception
+ throwable.printStackTrace(printWriter);
+
+ //Add "Xamarin Caused By" before the managed stacktrace. No new line after it.
+ printWriter.print(FIELD_XAMARIN_CAUSED_BY);
+ //print the stacktrace of the managed exception
+ printWriter.print(managedExceptionString);
+ } else {
+ //we have a java exception, no "Xamarin Caused By:"
+ throwable.printStackTrace(printWriter);
+ }
+ }
+
+ throwableStackTrace = stackTraceResult.toString();
+ }
+
+
+ public static CrashReport fromFile(File file) throws IOException {
+ String crashIdentifier = file.getName().substring(0, file.getName().indexOf(".stacktrace"));
+ return fromReader(crashIdentifier, new FileReader(file));
+ }
+
+ public static CrashReport fromReader(String crashIdentifier, Reader in) throws IOException {
+ BufferedReader bufferedReader = new BufferedReader(in);
+
+ CrashReport result = new CrashReport(crashIdentifier);
+
+ String readLine, headerName, headerValue;
+ boolean headersProcessed = false;
+ StringBuilder stackTraceBuilder = new StringBuilder();
+ while ((readLine = bufferedReader.readLine()) != null) {
+ if (!headersProcessed) {
+
+ if (readLine.isEmpty()) {
+ // empty line denotes break between headers and stack trace
+ headersProcessed = true;
+ continue;
+ }
+
+ int colonIndex = readLine.indexOf(":");
+ if (colonIndex < 0) {
+ AvalancheLog.error("Malformed header line when parsing crash details: \"" + readLine + "\"");
+ }
+
+ headerName = readLine.substring(0, colonIndex).trim();
+ headerValue = readLine.substring(colonIndex + 1, readLine.length()).trim();
+
+ if (headerName.equals(FIELD_CRASH_REPORTER_KEY)) {
+ result.setReporterKey(headerValue);
+ } else if (headerName.equals(FIELD_APP_START_DATE)) {
+ try {
+ result.setAppStartDate(DATE_FORMAT.parse(headerValue));
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ } else if (headerName.equals(FIELD_APP_CRASH_DATE)) {
+ try {
+ result.setAppCrashDate(DATE_FORMAT.parse(headerValue));
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ } else if (headerName.equals(FIELD_OS_VERSION)) {
+ result.setOsVersion(headerValue);
+ } else if (headerName.equals(FIELD_OS_BUILD)) {
+ result.setOsBuild(headerValue);
+ } else if (headerName.equals(FIELD_DEVICE_MANUFACTURER)) {
+ result.setDeviceManufacturer(headerValue);
+ } else if (headerName.equals(FIELD_DEVICE_MODEL)) {
+ result.setDeviceModel(headerValue);
+ } else if (headerName.equals(FIELD_APP_PACKAGE)) {
+ result.setAppPackage(headerValue);
+ } else if (headerName.equals(FIELD_APP_VERSION_NAME)) {
+ result.setAppVersionName(headerValue);
+ } else if (headerName.equals(FIELD_APP_VERSION_CODE)) {
+ result.setAppVersionCode(headerValue);
+ } else if (headerName.equals(FIELD_THREAD_NAME)) {
+ result.setThreadName(headerValue);
+ } else if (headerName.equals(FIELD_FORMAT)) {
+ result.setFormat(headerValue);
+ }
+
+ } else {
+ stackTraceBuilder.append(readLine).append("\n");
+ }
+ }
+ result.setThrowableStackTrace(stackTraceBuilder.toString());
+
+ return result;
+ }
+
+ //TODO this should not be in the model class
+ public void writeCrashReport() {
+ String path = Constants.FILES_PATH + "/" + crashIdentifier + ".stacktrace";
+ AvalancheLog.debug("Writing unhandled exception to: " + path);
+
+ BufferedWriter writer = null;
+
+ try {
+ writer = new BufferedWriter(new FileWriter(path));
+
+ writeHeader(writer, FIELD_APP_PACKAGE, appPackage);
+ writeHeader(writer, FIELD_APP_VERSION_CODE, appVersionCode);
+ writeHeader(writer, FIELD_APP_VERSION_NAME, appVersionName);
+ writeHeader(writer, FIELD_OS_VERSION, osVersion);
+ writeHeader(writer, FIELD_OS_BUILD, osBuild);
+ writeHeader(writer, FIELD_DEVICE_MANUFACTURER, deviceManufacturer);
+ writeHeader(writer, FIELD_DEVICE_MODEL, deviceModel);
+ writeHeader(writer, FIELD_THREAD_NAME, threadName);
+ writeHeader(writer, FIELD_CRASH_REPORTER_KEY, reporterKey);
+
+ writeHeader(writer, FIELD_APP_START_DATE, DATE_FORMAT.format(appStartDate));
+ writeHeader(writer, FIELD_APP_CRASH_DATE, DATE_FORMAT.format(appCrashDate));
+
+ if (isXamarinException) {
+ writeHeader(writer, FIELD_FORMAT, FIELD_FORMAT_VALUE);
+ }
+
+ writer.write("\n");
+ writer.write(throwableStackTrace);
+
+ writer.flush();
+
+ } catch (IOException e) {
+ AvalancheLog.error("Error saving crash report!", e);
+ } finally {
+ try {
+ if (writer != null) {
+ writer.close();
+ }
+ } catch (IOException e1) {
+ AvalancheLog.error("Error saving crash report!", e1);
+ }
+ }
+
+
+ }
+
+ private void writeHeader(Writer writer, String name, String value) throws IOException {
+ writer.write(name + ": " + value + "\n");
+ }
+
+ public String getCrashIdentifier() {
+ return crashIdentifier;
+ }
+
+ public String getReporterKey() {
+ return reporterKey;
+ }
+
+ public void setReporterKey(String reporterKey) {
+ this.reporterKey = reporterKey;
+ }
+
+ public Date getAppStartDate() {
+ return appStartDate;
+ }
+
+ public void setAppStartDate(Date appStartDate) {
+ this.appStartDate = appStartDate;
+ }
+
+ public Date getAppCrashDate() {
+ return appCrashDate;
+ }
+
+ public void setAppCrashDate(Date appCrashDate) {
+ this.appCrashDate = appCrashDate;
+ }
+
+ public String getOsVersion() {
+ return osVersion;
+ }
+
+ public void setOsVersion(String osVersion) {
+ this.osVersion = osVersion;
+ }
+
+ public String getOsBuild() {
+ return osBuild;
+ }
+
+ public void setOsBuild(String osBuild) {
+ this.osBuild = osBuild;
+ }
+
+ public String getDeviceManufacturer() {
+ return deviceManufacturer;
+ }
+
+ public void setDeviceManufacturer(String deviceManufacturer) {
+ this.deviceManufacturer = deviceManufacturer;
+ }
+
+ public String getDeviceModel() {
+ return deviceModel;
+ }
+
+ public void setDeviceModel(String deviceModel) {
+ this.deviceModel = deviceModel;
+ }
+
+ public String getAppPackage() {
+ return appPackage;
+ }
+
+ public void setAppPackage(String appPackage) {
+ this.appPackage = appPackage;
+ }
+
+ public String getAppVersionName() {
+ return appVersionName;
+ }
+
+ public void setAppVersionName(String appVersionName) {
+ this.appVersionName = appVersionName;
+ }
+
+ public String getAppVersionCode() {
+ return appVersionCode;
+ }
+
+ public void setAppVersionCode(String appVersionCode) {
+ this.appVersionCode = appVersionCode;
+ }
+
+ public String getThreadName() {
+ return threadName;
+ }
+
+ public void setThreadName(String threadName) {
+ this.threadName = threadName;
+ }
+
+ public String getThrowableStackTrace() {
+ return throwableStackTrace;
+ }
+
+ public void setThrowableStackTrace(String throwableStackTrace) {
+ this.throwableStackTrace = throwableStackTrace;
+ }
+
+ public Boolean getIsXamarinException() {
+ return isXamarinException;
+ }
+
+ public void setIsXamarinException(Boolean isXamarinException) {
+ this.isXamarinException = isXamarinException;
+ }
+
+ //We could to without a Format property and getters/setters, but we will eventually use this
+ public String getFormat() {
+ return format;
+ }
+
+ public void setFormat(String format) {
+ this.format = format;
+ }
+
+ @Override
+ public boolean isHighPriority() {
+ return true;
+ }
+}
diff --git a/crash/src/main/java/avalanche/crash/model/CrashesUserInput.java b/crash/src/main/java/avalanche/crash/model/CrashesUserInput.java
new file mode 100644
index 0000000000..dc540e2693
--- /dev/null
+++ b/crash/src/main/java/avalanche/crash/model/CrashesUserInput.java
@@ -0,0 +1,30 @@
+package avalanche.crash.model;
+
+/**
+ * Crash Manager user input
+ *
+ */
+public enum CrashesUserInput {
+ /**
+ * User chose not to send the crash report
+ */
+ CrashManagerUserInputDontSend(0),
+ /**
+ * User wants the crash report to be sent
+ */
+ CrashManagerUserInputSend(1),
+ /**
+ * User chose to always send crash reports
+ */
+ CrashManagerUserInputAlwaysSend(2);
+
+ private final int mValue;
+
+ CrashesUserInput(int value) {
+ this.mValue = value;
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+}
diff --git a/crash/src/main/res/values/strings.xml b/crash/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..eda1797c67
--- /dev/null
+++ b/crash/src/main/res/values/strings.xml
@@ -0,0 +1,9 @@
+
+
+ %s Unexpectedly Quit
+ Would you like to send an anonymous report so we can fix the problem?
+ Don\'t Send
+ Always Send
+ Send Report
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000000..6af8bfba6a
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+org.gradle.daemon=true
+
+# When configured, Gradle will run in incubating parallel mode.
+# 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
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..13372aef5e
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..122a0dca2e
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000000..9d82f78915
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000000..aec99730b4
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/sasquatch/.gitignore b/sasquatch/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/sasquatch/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/sasquatch/build.gradle b/sasquatch/build.gradle
new file mode 100644
index 0000000000..bbab99f834
--- /dev/null
+++ b/sasquatch/build.gradle
@@ -0,0 +1,37 @@
+apply plugin: 'com.android.application'
+//apply plugin: 'checkstyle'
+
+def supportLibVersion = rootProject.ext.supportLibVersion
+
+
+android {
+ compileSdkVersion rootProject.ext.compileSdkVersion
+ buildToolsVersion rootProject.ext.buildToolsVersion
+ defaultConfig {
+ applicationId "com.microsoft.android.avalanchesdk"
+ minSdkVersion rootProject.ext.minSdkVersion
+ targetSdkVersion rootProject.ext.targetSdkVersion
+ versionCode rootProject.ext.versionCode
+ versionName rootProject.ext.versionName
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ compile "com.android.support:appcompat-v7:$supportLibVersion"
+ testCompile 'junit:junit:4.12'
+ androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
+ androidTestCompile 'com.android.support.test:runner:0.5'
+ androidTestCompile "com.android.support:support-annotations:$supportLibVersion"
+
+ compile project(':base')
+ compile project(':crash')
+
+}
\ No newline at end of file
diff --git a/sasquatch/proguard-rules.pro b/sasquatch/proguard-rules.pro
new file mode 100644
index 0000000000..6e06453f42
--- /dev/null
+++ b/sasquatch/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/benny/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 *;
+#}
diff --git a/sasquatch/src/androidTest/java/avalanche/sasquatch/ExampleInstrumentationTest.java b/sasquatch/src/androidTest/java/avalanche/sasquatch/ExampleInstrumentationTest.java
new file mode 100644
index 0000000000..cd8486c9da
--- /dev/null
+++ b/sasquatch/src/androidTest/java/avalanche/sasquatch/ExampleInstrumentationTest.java
@@ -0,0 +1,29 @@
+package avalanche.sasquatch;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@MediumTest
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentationTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.microsoft.android.avalanchesdk", appContext.getPackageName());
+ }
+}
\ No newline at end of file
diff --git a/sasquatch/src/main/AndroidManifest.xml b/sasquatch/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..99da50ebf9
--- /dev/null
+++ b/sasquatch/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sasquatch/src/main/java/avalanche/sasquatch/MainActivity.java b/sasquatch/src/main/java/avalanche/sasquatch/MainActivity.java
new file mode 100644
index 0000000000..1fbfe3beec
--- /dev/null
+++ b/sasquatch/src/main/java/avalanche/sasquatch/MainActivity.java
@@ -0,0 +1,44 @@
+package avalanche.sasquatch;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import avalanche.base.AvalancheHub;
+import avalanche.base.utils.AvalancheLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MainActivity extends AppCompatActivity {
+
+ private Button mCrashButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ AvalancheHub.use(getApplication());
+
+ AvalancheLog.setLogLevel(Log.INFO);
+
+ boolean crashManagerAvailable = AvalancheHub.isFeatureAvailable(AvalancheHub.FEATURE_CRASH);
+
+ AvalancheLog.info("crash available: " + crashManagerAvailable);
+
+ boolean crashManagerEnabled = AvalancheHub.getSharedInstance().isFeatureEnabled(AvalancheHub.FEATURE_CRASH);
+
+ AvalancheLog.info("crash enabled: " + crashManagerEnabled);
+
+ mCrashButton = (Button) findViewById(R.id.button_crash);
+ mCrashButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ List fakeList = new ArrayList<>();
+ fakeList.get(1000);
+ }
+ });
+ }
+}
diff --git a/sasquatch/src/main/res/layout/activity_main.xml b/sasquatch/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000000..79a813a488
--- /dev/null
+++ b/sasquatch/src/main/res/layout/activity_main.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/sasquatch/src/main/res/mipmap-hdpi/ic_launcher.png b/sasquatch/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..cde69bccce
Binary files /dev/null and b/sasquatch/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sasquatch/src/main/res/mipmap-mdpi/ic_launcher.png b/sasquatch/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..c133a0cbd3
Binary files /dev/null and b/sasquatch/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sasquatch/src/main/res/mipmap-xhdpi/ic_launcher.png b/sasquatch/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..bfa42f0e7b
Binary files /dev/null and b/sasquatch/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sasquatch/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sasquatch/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..324e72cdd7
Binary files /dev/null and b/sasquatch/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sasquatch/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sasquatch/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..aee44e1384
Binary files /dev/null and b/sasquatch/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sasquatch/src/main/res/values-w820dp/dimens.xml b/sasquatch/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000000..63fc816444
--- /dev/null
+++ b/sasquatch/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/sasquatch/src/main/res/values/colors.xml b/sasquatch/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..3ab3e9cbce
--- /dev/null
+++ b/sasquatch/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/sasquatch/src/main/res/values/dimens.xml b/sasquatch/src/main/res/values/dimens.xml
new file mode 100644
index 0000000000..47c8224673
--- /dev/null
+++ b/sasquatch/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 16dp
+ 16dp
+
diff --git a/sasquatch/src/main/res/values/strings.xml b/sasquatch/src/main/res/values/strings.xml
new file mode 100644
index 0000000000..f0b9788bfa
--- /dev/null
+++ b/sasquatch/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+ Sasquatch App
+ Crash me
+
diff --git a/sasquatch/src/main/res/values/styles.xml b/sasquatch/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..5885930df6
--- /dev/null
+++ b/sasquatch/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/sasquatch/src/test/java/avalanche/sasquatch/ExampleUnitTest.java b/sasquatch/src/test/java/avalanche/sasquatch/ExampleUnitTest.java
new file mode 100644
index 0000000000..e9a03a8fc2
--- /dev/null
+++ b/sasquatch/src/test/java/avalanche/sasquatch/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package avalanche.sasquatch;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000000..52a169661a
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':base', ':crash', ':sasquatch', ':analytics'
\ No newline at end of file