diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..f7cc59c --- /dev/null +++ b/Gemfile @@ -0,0 +1,10 @@ +source 'https://www.rubygems.org' + +gem 'appium_lib', '~> 6.0.0' +gem 'rest-client', '~> 1.8.0' +gem 'rspec', '~> 2.14.1' +gem 'rspec-expectations', '~> 2.14.5' +gem 'spec', '~> 5.3.4' +gem 'sauce_whisk', '~> 0.0.14' +gem 'test-unit', '~> 2.5.5' # required for bundle exec ruby xunit_android.rb +gem 'byebug', '~> 4.0.5' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d31d5a9 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,80 @@ +GEM + remote: https://www.rubygems.org/ + specs: + appium_lib (6.0.0) + awesome_print (~> 1.6, >= 1.6.0) + json (~> 1.8, >= 1.8.1) + nokogiri (~> 1.6.3.1) + selenium-webdriver (~> 2.41, >= 2.41.0) + toml (~> 0.0, >= 0.0.4) + awesome_print (1.6.1) + blankslate (2.1.2.4) + byebug (4.0.5) + columnize (= 0.9.0) + childprocess (0.5.6) + ffi (~> 1.0, >= 1.0.11) + chronic_duration (0.10.6) + numerizer (~> 0.1.1) + columnize (0.9.0) + diff-lcs (1.2.5) + domain_name (0.5.24) + unf (>= 0.0.5, < 1.0.0) + ffi (1.9.8) + http-cookie (1.0.2) + domain_name (~> 0.5) + json (1.8.2) + mime-types (2.5) + mini_portile (0.6.0) + multi_json (1.11.0) + netrc (0.10.3) + nokogiri (1.6.3.1) + mini_portile (= 0.6.0) + numerizer (0.1.1) + parslet (1.5.0) + blankslate (~> 2.0) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + rspec (2.14.1) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rspec-core (2.14.8) + rspec-expectations (2.14.5) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.6) + rubyzip (1.1.7) + sauce_whisk (0.0.18) + json (~> 1.8.1) + rest-client (~> 1.7) + selenium-webdriver (2.45.0) + childprocess (~> 0.5) + multi_json (~> 1.0) + rubyzip (~> 1.0) + websocket (~> 1.0) + spec (5.3.4) + chronic_duration (~> 0.10.2) + test-unit (2.5.5) + toml (0.1.2) + parslet (~> 1.5.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.1) + websocket (1.2.2) + +PLATFORMS + ruby + +DEPENDENCIES + appium_lib (~> 6.0.0) + byebug (~> 4.0.5) + rest-client (~> 1.8.0) + rspec (~> 2.14.1) + rspec-expectations (~> 2.14.5) + sauce_whisk (~> 0.0.14) + spec (~> 5.3.4) + test-unit (~> 2.5.5) + +BUNDLED WITH + 1.10.3 diff --git a/README.md b/README.md index c3df6c1..3c5508c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [ ![Download](https://api.bintray.com/packages/appinsights-android/maven/ApplicationInsights-Android/images/download.svg) ](https://bintray.com/appinsights-android/maven/ApplicationInsights-Android/_latestVersion) -# Application Insights for Android (1.0-beta.4) +# Application Insights for Android (1.0-beta.5) This project provides an Android SDK for Application Insights. [Application Insights](http://azure.microsoft.com/en-us/services/application-insights/) is a service that allows developers to keep their applications available, performing, and succeeding. This module allows you to send telemetry of various kinds (events, traces, exceptions, etc.) to the Application Insights service where your data can be visualized in the Azure Portal. @@ -11,7 +11,7 @@ Automatic collection of lifecycle-events requires API level 15 and up (Ice Cream ## Content 1. [Release Notes](#1) -2. [Breaking Changes](#2) +2. [Breaking Changes & Deprecations](#2) 3. [Setup](#3) 4. [Advanced Setup](#4) 5. [Developer Mode](#5) @@ -21,32 +21,47 @@ Automatic collection of lifecycle-events requires API level 15 and up (Ice Cream 9. [Additional configuration](#9) 10. [Documentation](#10) 11. [Contributing](#11) +12. [Contact](#12) ## 1. Release Notes -* Improvements regarding threat safety -* Improved unit tests (now using Mockito) -* Simplified threading model (still deferring work to background tasks) -* Bugfix for sending logic (number of running operations wasn't decremented when we don't have a connection) -* Fix for potential memory leaks -* Updated code in sample app -* Data is now persisted when the user sends the app into the background (requires API level 14) -* Data is now persisted when the device is low on memory +* The SDK is now built using the Android Tools Gradle plugin 1.2.3 +* Fix a null pointer exception in ```LifecycleTracking```[#43](https://github.com/Microsoft/ApplicationInsights-Android/pull/43) +* Refactored Autocollection – ```LifecycleTracking```has been deprecated [#51](https://github.com/Microsoft/ApplicationInsights-Android/pull/51) +* Fix for null pointer exceptions when trying to serialize ```null``` [#45](https://github.com/Microsoft/ApplicationInsights-Android/pull/45) +* Fix for ```Concurrent Modification Exception``` in case the same Telemetry-Object was after it was modified [#44](https://github.com/Microsoft/ApplicationInsights-Android/pull/44) +* **Fix for ```ClassNotFoundException``` when running the SDK on an Android 2.3 device** [#48](https://github.com/Microsoft/ApplicationInsights-Android/pull/48) +* **Fix a bug that was introduced in 1.0-beta.4 that caused crashes not to be sent under some circumstances** [#52](https://github.com/Microsoft/ApplicationInsights-Android/pull/52) & [e3b51e7927f238cc123c50b654fbeab448ba6df6](https://github.com/Microsoft/ApplicationInsights-Android/commit/e3b51e7927f238cc123c50b654fbeab448ba6df6) -## 2. Breaking Changes -Starting with the first 1.0 stable release, we will start deprecating API instead of breaking old ones. +## 2. Breaking Changes & deprecations + +Starting with 1.0-beta.5, breaking changes will be announced 1 release in advance. Once a method has been deprecated, the next release of the SDK will remove the API. + +**[1.0-beta.5]** + +* Two previously deprecated setup-methods for ```ApplicationInsights```have been removed. +* ```LifecycleTracking```has been deprecated, use ```AutoCollection```instead. + +**[1.0-beta.4]** -* **[1.0-beta.4]** **No breaking API changes**. * Two setup-methods for ```ApplicationInsights```have been deprecated and will be removed in the next beta -* **[1.0-beta.3]** Configuration of the Application Insights SDK is now done using ```ApplicationInsightsConfig```. The previous config-classes have been removed +**[1.0-beta.3]** + +Configuration of the Application Insights SDK is now done using ```ApplicationInsightsConfig```. The previous config-classes have been removed + +**[1.0-beta.2]** + +To enable automatic lifecycle-tracking, Application Insights has to be set up with an instance of Application (see [Life-cycle tracking] (#2)), otherwise, lifecycle-tracking is disabled. + +**[1.0-beta.1]** -* **[1.0-beta.2]** To enable automatic lifecycle-tracking, Application Insights has to be set up with an instance of Application (see [Life-cycle tracking] (#2)), otherwise, lifecycle-tracking is disabled. +Setup and start of the Application Insights SDK are now done using the new umbrella class `ApplicationInsights` instead of `AppInsights ` -* **[1.0-beta.1]** Setup and start of the Application Insights SDK are now done using the new umbrella class `ApplicationInsights` instead of `AppInsights ` +**[1.0-Alpha.5]** -* **[1.0-Alpha.5]** Setup and start of the Application Insights SDK are now done using the new umbrella class `AppInsights` instead of `TelemetryClient` +Setup and start of the Application Insights SDK are now done using the new umbrella class `AppInsights` instead of `TelemetryClient` ## 3. Setup @@ -60,7 +75,7 @@ In your module's ```build.gradle```add a dependency for Application Insights ```groovy dependencies { - compile 'com.microsoft.azure:applicationinsights-android:1.0-beta.4' + compile 'com.microsoft.azure:applicationinsights-android:1.0-beta.5' } ``` @@ -110,7 +125,7 @@ ApplicationInsights.start(); in the activity's `onCreate`-callback. -**Congratulation, now you're all set to use Application Insights! See [Usage](#6) how to use Application Insights.** +**Congratulation, now you're all set to use Application Insights! See [Usage](#6) on how to use Application Insights.** ## 4. Advanced Setup @@ -146,7 +161,7 @@ ApplicationInsights.start(); ## 5. Developer Mode -The **developer mode** is enabled automatically in case the debugger is attached or if the app is running in the emulator. This will enable the console logging and decrease the number of telemetry items sent in a batch (5 items) as well as the interval items will be sent (3 seconds). If you don't want this behavior, disable the **developer mode**. +The **developer mode** is enabled automatically in case the debugger is attached or if the app is running in the emulator. This will enable the console logging and decrease the number of telemetry items per batch (5 items) as well as the sending interval (3 seconds). If you don't want this behavior, disable the **developer mode**. You can explicitly enable/disable the developer mode like this: @@ -206,7 +221,7 @@ client.trackEvent("sample event", properties); ## 7. Automatic collection of life-cycle events (Sessions & Page Views) -This only works in Android SDK version 15 and up (Ice Cream Sandwich+) and is **enabled by default**. Don't forget to provide an Application instance when setting up Application Insights (otherwise auto collection will be disabled): +This only works in Android SDK version 14 and up (Ice Cream Sandwich+) and is **enabled by default**. Don't forget to provide an Application instance when setting up Application Insights (otherwise auto collection will be disabled): ```java ApplicationInsights.setup(this.getApplicationContext(), this.getApplication()); @@ -220,7 +235,7 @@ ApplicationInsights.setAutoCollectionDisabled(true); //disable the auto-collecti ApplicationInsights.start(); ``` -After `ApplicationInsights.start()` was called, you can enable or disable those features at any point: +After `ApplicationInsights.start()` was called, you can enable or disable those features at any point, even if you have disabled it between setup and start of the Application Insights SDK: ```java // Disable automatic session renewal & tracking @@ -307,7 +322,7 @@ ApplicationInsights.renewSession("New session ID"); ### 9.5 Other -For all available configarion options, see our [Javadoc](http://microsoft.github.io/ApplicationInsights-Android/) for ```ApplicationInisghtsConfig``` +For all available configarion options, see our [Javadoc](http://microsoft.github.io/ApplicationInsights-Android/) for ```ApplicationInsightsConfig``` ## 10. Documentation @@ -322,8 +337,7 @@ Our Javadoc can be found at [http://microsoft.github.io/ApplicationInsights-Andr * [Get an instrumentation key](/Microsoft/ApplicationInsights-Home/wiki#getting-an-application-insights-instrumentation-key) and set it in the manifest * Run tests from Android Studio - -## 12. Contact +## 12. Contact If you have further questions or are running into trouble that cannot be resolved by any of the steps here, feel free to contact us at [AppInsights-Android@microsoft.com](mailto:AppInsights-Android@microsoft.com) diff --git a/app-sample/src/main/java/com/microsoft/applicationinsights/appsample/ItemListFragment.java b/app-sample/src/main/java/com/microsoft/applicationinsights/appsample/ItemListFragment.java index 83f7529..b896875 100644 --- a/app-sample/src/main/java/com/microsoft/applicationinsights/appsample/ItemListFragment.java +++ b/app-sample/src/main/java/com/microsoft/applicationinsights/appsample/ItemListFragment.java @@ -72,8 +72,6 @@ public void onItemSelected(String id) { public ItemListFragment() { } - @SuppressLint("InlinedApi") - @TargetApi(11) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -81,7 +79,7 @@ public void onCreate(Bundle savedInstanceState) { // TODO: replace with a real list adapter. setListAdapter(new ArrayAdapter( getActivity(), - android.R.layout.simple_list_item_activated_1, + android.R.layout.simple_list_item_1, android.R.id.text1, DummyContent.ITEMS)); } diff --git a/appium/integration/prototype_spec.rb b/appium/integration/prototype_spec.rb new file mode 100644 index 0000000..fa2e4e2 --- /dev/null +++ b/appium/integration/prototype_spec.rb @@ -0,0 +1,116 @@ +require File.expand_path('spec_helper') + +def defaultTestRun + + it 'should should tap track 5 times' do + list_el = text('Track event') + list_el.click + back + list_el.click + back + list_el.click + back + list_el.click + back + list_el.click + back + end + +it 'should background for 3s and foreground again 3 times' do + background_app(3) + sleep(2) + background_app(3) + sleep(2) + background_app(3) +end + +it 'should should tap track 5 times' do + list_el = text('Track event') + list_el.click + back + list_el.click + back + list_el.click + back + list_el.click + back + list_el.click + back + end + + it 'should background for 10s and foreground again' do + background_app(10) + sleep(2) +end + + it 'should trigger a sync' do + list_el = text('Trigger Synchronize') + list_el.click + back + sleep(5) + list_el.click + back + end + +it 'should crash the app' do + list_el = text('Crash the App!') + list_el.click +end + +end + +describe 'Run default tests' do + defaultTestRun +end + + +describe 'Run with disabled session management' do + it 'Can disable session management' do + list_el = text('Disable session management') + list_el.click + back +end + defaultTestRun +end + +describe 'Run with re-enabled session management' do + it 'Can enable session management' do + list_el = text('Enable session management') + list_el.click + back +end + defaultTestRun +end + +describe 'Run with disabled pageviews' do + it 'Can disable pageviews' do + list_el = text('Disable page view tracking') + list_el.click + back +end + defaultTestRun +end + +describe 'Run with re-enabled pageviews' do + it 'Can re-enable pageviews' do + list_el = text('Enable page view tracking') + list_el.click + back +end + defaultTestRun +end + +describe 'Run with disabled pageviews and session management' do + it 'Can disable pageviews' do + list_el = text('Disable page view tracking') + list_el.click + back +end + it 'Can disable session management' do + list_el = text('Disable session management') + list_el.click + back +end + defaultTestRun +end + diff --git a/appium/spec_helper.rb b/appium/spec_helper.rb new file mode 100644 index 0000000..48de8fc --- /dev/null +++ b/appium/spec_helper.rb @@ -0,0 +1,27 @@ +require 'rubygems' +require 'rspec' +require 'rspec/expectations' +require 'appium_lib' +require 'byebug' + +RSpec.configure do |config| + config.color_enabled = true + config.before(:all) do + options = { + caps: { + platformName: 'Android', + app:'../app-sample/build/outputs/apk/app-sample-debug.apk', + deviceName: 'appinsights-appium' #required but doesn't have to match for genymotion or android emu + }, + launchTimeout: 5000 + } + + driver = Appium::Driver.new(options).start_driver + driver.manage.timeouts.implicit_wait = 10 + Appium.promote_appium_methods Object + end + + config.after(:all) do + driver_quit + end +end \ No newline at end of file diff --git a/applicationinsights-android/build.gradle b/applicationinsights-android/build.gradle index a8910cd..29d8200 100644 --- a/applicationinsights-android/build.gradle +++ b/applicationinsights-android/build.gradle @@ -112,8 +112,6 @@ if (!this.hasProperty('bintray_user') || dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:appcompat-v7:22.0.0' - //noinspection GradleDynamicVersion androidTestCompile 'org.mockito:mockito-core:2.+' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' } diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelQueueTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelQueueTest.java index 7bfd454..743e326 100644 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelQueueTest.java +++ b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelQueueTest.java @@ -46,8 +46,8 @@ public void testItemGetsEnqueued(){ when(mockConfig.getMaxBatchCount()).thenReturn(3); // Test - sut.enqueue(new Envelope()); - sut.enqueue(new Envelope()); + sut.enqueue(new String()); + sut.enqueue(new String()); // Verify Assert.assertEquals(2, sut.list.size()); @@ -59,17 +59,17 @@ public void testQueueFlushedIfMaxBatchCountReached() { when(mockConfig.getMaxBatchCount()).thenReturn(3); // Test - sut.enqueue(new Envelope()); - sut.enqueue(new Envelope()); + sut.enqueue(new String()); + sut.enqueue(new String()); // Verify Assert.assertEquals(2, sut.list.size()); - verify(mockPersistence,never()).persist(any(IJsonSerializable[].class), anyBoolean()); + verify(mockPersistence,never()).persist(any(String[].class), anyBoolean()); - sut.enqueue(new Envelope()); + sut.enqueue(new String()); Assert.assertEquals(0, sut.list.size()); - verify(mockPersistence,times(1)).persist(any(IJsonSerializable[].class), anyBoolean()); + verify(mockPersistence,times(1)).persist(any(String[].class), anyBoolean()); } public void testQueueFlushedAfterBatchIntervalReached() { @@ -78,12 +78,12 @@ public void testQueueFlushedAfterBatchIntervalReached() { when(mockConfig.getMaxBatchCount()).thenReturn(3); // Test - sut.enqueue(new Envelope()); + sut.enqueue(new String()); // Verify Assert.assertEquals(1, sut.list.size()); - verify(mockPersistence,never()).persist(any(IJsonSerializable[].class), anyBoolean()); - verify(mockPersistence,after(250).times(1)).persist(any(IJsonSerializable[].class), anyBoolean()); + verify(mockPersistence,never()).persist(any(String[].class), anyBoolean()); + verify(mockPersistence,after(250).times(1)).persist(any(String[].class), anyBoolean()); Assert.assertEquals(0, sut.list.size()); } @@ -92,16 +92,16 @@ public void testFlushingQueueWorks() { when(mockConfig.getMaxBatchIntervalMs()).thenReturn(200); when(mockConfig.getMaxBatchCount()).thenReturn(3); - sut.enqueue(new Envelope()); + sut.enqueue(new String()); Assert.assertEquals(1, sut.list.size()); - verify(mockPersistence,never()).persist(any(IJsonSerializable[].class), anyBoolean()); + verify(mockPersistence,never()).persist(any(String[].class), anyBoolean()); // Test sut.flush(); // Verify Assert.assertEquals(0, sut.list.size()); - verify(mockPersistence,times(1)).persist(any(IJsonSerializable[].class), anyBoolean()); + verify(mockPersistence,times(1)).persist(any(String[].class), anyBoolean()); } diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelTest.java index 8bff339..6a3eed8 100644 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelTest.java +++ b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ChannelTest.java @@ -34,13 +34,17 @@ public void testSynchronizeFlushesQueue(){ public void testEnqueuedItemIsAddedToQueue(){ // Test Envelope testItem1 = new Envelope(); + testItem1.setDeviceId("Test"); + String serialized1 = sut.serializeEnvelope(testItem1); sut.enqueue(testItem1); Envelope testItem2 = new Envelope(); + testItem2.setDeviceId("Test1"); + String serialized2 = sut.serializeEnvelope(testItem2); sut.enqueue(testItem2); // Verify - verify(mockQueue, times(1)).enqueue(testItem1); - verify(mockQueue, times(1)).enqueue(testItem2); + verify(mockQueue, times(1)).enqueue(serialized1); + verify(mockQueue, times(1)).enqueue(serialized2); } public void testProcessUnhandledExceptionIsPersistedDirectly(){ @@ -49,19 +53,20 @@ public void testProcessUnhandledExceptionIsPersistedDirectly(){ sut.processUnhandledException(testItem1); // Verify - verify(mockQueue, times(0)).enqueue(testItem1); - verify(mockPersistence, times(1)).persist(any(IJsonSerializable[].class), eq(true)); + verify(mockQueue, times(0)).enqueue(new String()); + verify(mockPersistence, times(1)).persist(any(String[].class), eq(true)); } public void testQueueFlushesWhenProcessingCrash(){ // Setup Envelope testItem1 = new Envelope(); + String serializedString = sut.serializeEnvelope(testItem1); // Test sut.processUnhandledException(testItem1); // Verify - verify(mockQueue, times(0)).enqueue(testItem1); + verify(mockQueue, times(0)).enqueue(serializedString); verify(mockQueue, times(1)).flush(); } } diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ExceptionTrackingTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ExceptionTrackingTest.java deleted file mode 100644 index 8cc5110..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/ExceptionTrackingTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.content.Intent; -import android.test.ActivityUnitTestCase; - -import junit.framework.Assert; - -public class ExceptionTrackingTest extends ActivityUnitTestCase { - - public Thread.UncaughtExceptionHandler originalHandler; - public ExceptionTrackingTest() { - super(MockActivity.class); - } - - public void setUp() throws Exception { - super.setUp(); - originalHandler = Thread.getDefaultUncaughtExceptionHandler(); - Intent intent = new Intent(getInstrumentation().getTargetContext(), MockActivity.class); - this.setActivity(this.startActivity(intent, null, null)); - } - - public void tearDown() throws Exception { - super.tearDown(); - Thread.setDefaultUncaughtExceptionHandler(originalHandler); - Channel.getInstance().queue.setIsCrashing(false); - ApplicationInsights.setDeveloperMode(false); - } - - public void testRegisterExceptionHandler() throws Exception { - ExceptionTracking.registerExceptionHandler(); - Thread.UncaughtExceptionHandler handler = - Thread.getDefaultUncaughtExceptionHandler(); - Assert.assertNotNull("handler is set", handler); - Assert.assertEquals("handler is of correct type", ExceptionTracking.class, handler.getClass()); - - // double register without debug mode - ApplicationInsights.setDeveloperMode(false); - ExceptionTracking.registerExceptionHandler(); - Assert.assertTrue("no exception for multiple registration without debug mode", true); - - // double register with debug mode and verify runtime exception - ApplicationInsights.setDeveloperMode(true); - RuntimeException exception = null; - try { - ExceptionTracking.registerExceptionHandler(); - } catch (RuntimeException e) { - exception = e; - } - - Assert.assertNotNull("developer Exception was thrown", exception); - } -} \ No newline at end of file diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/LifeCycleTrackingTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/LifeCycleTrackingTest.java deleted file mode 100644 index 72690cb..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/LifeCycleTrackingTest.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.annotation.TargetApi; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; -import android.os.Build; -import android.os.IBinder; -import android.test.ActivityUnitTestCase; - -import com.microsoft.applicationinsights.contracts.Data; -import com.microsoft.applicationinsights.contracts.Envelope; -import com.microsoft.applicationinsights.contracts.SessionState; -import com.microsoft.applicationinsights.contracts.SessionStateData; -import com.microsoft.applicationinsights.contracts.shared.ITelemetryData; - -import junit.framework.Assert; - -import java.util.ArrayList; - -@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) -public class LifeCycleTrackingTest extends ActivityUnitTestCase { - private Intent intent; - private MockTelemetryClient telemetryClient; - private MockApplication mockApplication; - private MockLifeCycleTracking mockLifeCycleTracking; - - public LifeCycleTrackingTest() { - super(MockActivity.class); - } - - //TODO fix Mock object if easily possible - - - public void setUp() throws Exception { - super.setUp(); - - Context context = this.getInstrumentation().getContext(); - this.mockLifeCycleTracking = MockLifeCycleTracking.getInstance(); - this.mockLifeCycleTracking.reset(); - this.telemetryClient = this.mockLifeCycleTracking.tc; - - this.mockApplication = new MockApplication(context); - this.mockApplication.onCreate(); - this.setApplication(this.mockApplication); - - this.intent = new Intent(context, MockActivity.class); - - this.telemetryClient.clearMessages(); - } - - public void tearDown() throws Exception { - this.mockApplication.unregister(); - } - - public void testPageViewEvent() throws Exception { - // setup - MockActivity activity = this.startActivity(this.intent, null, null); - - // test - getInstrumentation().callActivityOnResume(activity); - - // validation - ArrayList messages = this.mockLifeCycleTracking.tc.getMessages(); - - Assert.assertEquals("Received 2 messages", 2, messages.size()); - SessionStateData sessionData = (SessionStateData)((Data) messages.get(0).getData()).getBaseData(); - Assert.assertEquals("Received Session State data", "Microsoft.ApplicationInsights.SessionStateData", sessionData.getBaseType()); - Assert.assertEquals("Got the start session", SessionState.Start, sessionData.getState()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(1).getData().getBaseType()); - } - - public void testPageViewEventMultipleActivities() throws Exception { - MockActivity activity1 = this.startActivity(this.intent, null, null); - MockActivity activity2 = this.getMockActivity(new Intent( - this.getInstrumentation().getContext(), - MockActivity.class), MockActivity.class); - MockActivity activity3 = this.getMockActivity(new Intent( - this.getInstrumentation().getContext(), - MockActivity.class), MockActivity.class); - - // test - getInstrumentation().callActivityOnResume(activity1); - getInstrumentation().callActivityOnResume(activity2); - getInstrumentation().callActivityOnResume(activity3); - getInstrumentation().callActivityOnResume(activity2); - getInstrumentation().callActivityOnResume(activity1); - - // validation - ArrayList messages = this.mockLifeCycleTracking.tc.getMessages(); - Assert.assertEquals("Received 6 messages", 6, messages.size()); - Assert.assertEquals("Received Session State data", "Microsoft.ApplicationInsights.SessionStateData", messages.get(0).getData().getBaseType()); - - SessionStateData sessionData = (SessionStateData) ((Data) messages.get(0).getData()).getBaseData(); - Assert.assertEquals("Got the start session", SessionState.Start, sessionData.getState()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(1).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(2).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(3).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(4).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(5).getData().getBaseType()); - } - - public void testOnStartEvent() throws Exception { - // setup - MockActivity activity = this.startActivity(this.intent, null, null); - - // test that on start event fires first time - getInstrumentation().callActivityOnResume(activity); - - // validation - ArrayList messages = this.mockLifeCycleTracking.tc.getMessages(); - Assert.assertEquals("Received 2 messages", 2, messages.size()); - Assert.assertEquals("Received Session State data", "Microsoft.ApplicationInsights.SessionStateData", messages.get(0).getData().getBaseType()); - - SessionStateData sessionData = (SessionStateData) ((Data) messages.get(0).getData()).getBaseData(); - Assert.assertEquals("Got the start session", SessionState.Start, sessionData.getState()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(1).getData().getBaseType()); - - // test that on start event doesn't fire second time - getInstrumentation().callActivityOnResume(activity); - - // validation - messages = this.mockLifeCycleTracking.tc.getMessages(); - Assert.assertEquals("Received 3 message2", 3, messages.size()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(2).getData().getBaseType()); - } - - public void testOnStartEventMultipleActivities() throws Exception { - // setup - MockActivity activity1 = this.startActivity(this.intent, null, null); - MockActivity activity2 = this.getMockActivity(new Intent( - this.getInstrumentation().getContext(), - MockActivity.class), MockActivity.class); - MockActivity activity3 = this.getMockActivity(new Intent( - this.getInstrumentation().getContext(), - MockActivity.class), MockActivity.class); - - // test - getInstrumentation().callActivityOnResume(activity1); - getInstrumentation().callActivityOnResume(activity2); - getInstrumentation().callActivityOnResume(activity3); - getInstrumentation().callActivityOnResume(activity1); - - // validation - ArrayList messages = this.mockLifeCycleTracking.tc.getMessages(); - Assert.assertEquals("Received 5 messages", 5, messages.size()); - Assert.assertEquals("Received Session State data", "Microsoft.ApplicationInsights.SessionStateData", messages.get(0).getData().getBaseType()); - - SessionStateData sessionData = (SessionStateData) ((Data) messages.get(0).getData()).getBaseData(); - Assert.assertEquals("Got the start session", SessionState.Start, sessionData.getState()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(1).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(2).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(3).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(4).getData().getBaseType()); - - } - - public void testSessionTimeout() throws Exception { - // setup - MockActivity activity1 = this.startActivity(this.intent, null, null); - MockActivity activity2 = this.getMockActivity(new Intent( - this.getInstrumentation().getContext(), - MockActivity.class), MockActivity.class); - - // test 3 activities starting/stopping then restarting the first - getInstrumentation().callActivityOnResume(activity1); - getInstrumentation().callActivityOnResume(activity2); - - this.mockLifeCycleTracking.currentTime += ApplicationInsights.getConfig().getSessionIntervalMs(); - getInstrumentation().callActivityOnResume(activity1); - getInstrumentation().callActivityOnResume(activity2); - - // validation - ArrayList messages = this.mockLifeCycleTracking.tc.getMessages(); - Assert.assertEquals("Received 6 messages", 6, messages.size()); - Assert.assertEquals("Received Session State data", "Microsoft.ApplicationInsights.SessionStateData", messages.get(0).getData().getBaseType()); - - SessionStateData sessionData = (SessionStateData) ((Data) messages.get(0).getData()).getBaseData(); - Assert.assertEquals("Got the start session", SessionState.Start, sessionData.getState()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(1).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(2).getData().getBaseType()); - Assert.assertEquals("Received Session State data", "Microsoft.ApplicationInsights.SessionStateData", messages.get(3).getData().getBaseType()); - - sessionData = (SessionStateData) ((Data) messages.get(3).getData()).getBaseData(); - Assert.assertEquals("Got the start session", SessionState.Start, sessionData.getState()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(4).getData().getBaseType()); - Assert.assertEquals("Received page view", "Microsoft.ApplicationInsights.PageViewData", messages.get(5).getData().getBaseType()); - } - - private MockActivity getMockActivity(Intent intent, Class activityClass) { - IBinder token = null; - setApplication(new android.test.mock.MockApplication()); - ComponentName cn = new ComponentName(activityClass.getPackage().getName(), - activityClass.getName()); - intent.setComponent(cn); - ActivityInfo info = new ActivityInfo(); - CharSequence title = activityClass.getName(); - - String id = null; - - MockActivity activity = null; - try { - activity = (MockActivity) getInstrumentation().newActivity( - activityClass, - this.getInstrumentation().getContext(), - token, this.mockApplication, intent, info, - title, this.getActivity().getParent(), id, null); - } catch (InstantiationException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - return activity; - } -} diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockActivity.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockActivity.java index 9bd6699..cbdb3b8 100644 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockActivity.java +++ b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockActivity.java @@ -3,6 +3,7 @@ import android.app.Activity; public class MockActivity extends Activity { + public MockActivity() { super(); } diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockApplication.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockApplication.java deleted file mode 100644 index d8bc3f9..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockApplication.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.app.Application; -import android.content.Context; -import android.os.Build; - -public class MockApplication extends Application { - Context context; - - public MockApplication(Context context) { - this.context = context; - } - - @Override - public Context getApplicationContext() { - return this.context; - } - - @Override - public void onCreate() { - super.onCreate(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - registerActivityLifecycleCallbacks(MockLifeCycleTracking.getInstance()); - } - } - - public void unregister() { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - unregisterActivityLifecycleCallbacks(MockLifeCycleTracking.getInstance()); - } - } -} \ No newline at end of file diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockLifeCycleTracking.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockLifeCycleTracking.java deleted file mode 100644 index e58663a..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockLifeCycleTracking.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.app.Activity; - -import com.microsoft.applicationinsights.library.config.ISessionConfig; - -public class MockLifeCycleTracking extends LifeCycleTracking { - - //TODO check this implementation - - public final MockTelemetryClient tc; - public long currentTime; - - public static MockLifeCycleTracking getInstance() { - return (MockLifeCycleTracking)LifeCycleTracking.getInstance(); - } - - protected MockLifeCycleTracking(ISessionConfig config, TelemetryContext telemetryContext) { - super(config, telemetryContext); - currentTime = 0; - this.tc = new MockTelemetryClient(true); - } - - //@Override - protected TelemetryClient getTelemetryClient(Activity activity) { - return this.tc; - } - - @Override - protected long getTime() { - return currentTime; - } - - public void reset() { - this.currentTime = 0; - this.tc.clearMessages(); - super.activityCount.set(0); - super.lastBackground.set(0); - } -} diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockTelemetryClient.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockTelemetryClient.java index d2f5f58..6dbf56e 100644 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockTelemetryClient.java +++ b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/MockTelemetryClient.java @@ -6,7 +6,7 @@ import java.util.Map; public class MockTelemetryClient extends TelemetryClient { - public ArrayList messages; + public ArrayList messages = new ArrayList(); public boolean mockTrackMethod; /** diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/PersistenceTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/PersistenceTest.java index 48d3301..8b48b74 100644 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/PersistenceTest.java +++ b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/PersistenceTest.java @@ -29,7 +29,7 @@ public void testSaveAndGetData() throws Exception { Persistence persistence = Persistence.getInstance(); String data = "SAVE THIS DATA"; - persistence.persist(data, false); + persistence.writeToDisk(data, false); File file = persistence.nextAvailableFile(); Assert.assertEquals("Data retrieved from file is equal to data saved", data, persistence.load(file)); } diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryClientTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryClientTest.java deleted file mode 100644 index 709b249..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryClientTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.content.Intent; -import android.test.ActivityUnitTestCase; - -import junit.framework.Assert; - -public class TelemetryClientTest extends ActivityUnitTestCase { - - MockActivity mockActivity; - - public TelemetryClientTest() { - super(MockActivity.class); - } - - - public void setUp() throws Exception { - super.setUp(); - - Intent intent = new Intent(getInstrumentation().getTargetContext(), MockActivity.class); - this.mockActivity = this.startActivity(intent, null, null); - } - - public void testRegister() throws Exception { - TelemetryClient client = TelemetryClient.getInstance(); - Assert.assertNotNull("static registration returns non-null client", client); - } - - //TODO test at applicable location -// public void testGetContext() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(); -// Assert.assertNotNull("context is initialized", client.getWeakContext()); -// } - - //TODO test at applicable location -// public void testGetConfig() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(this.mockActivity); -// Assert.assertNotNull("config is initialized", client.getConfig()); -// } - - //TODO test at applicable location -// public void testCommonProperties() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(this.mockActivity); -// -// // add a property -// LinkedHashMap properties1 = new LinkedHashMap<>(); -// properties1.put("p1", "v1"); -// client.setCommonProperties(properties1); -// -// // check that it exists -// Map properties2 = client.getCommonProperties(); -// Assert.assertEquals("Property 1 matches", "v1", properties2.get("p1")); -// } - -// public void testTrackEvent() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(); -// LinkedHashMap properties = new LinkedHashMap<>(); -// LinkedHashMap measurements = new LinkedHashMap<>(); -// -// client.trackEvent(null); -// client.trackEvent("event1"); -// client.trackEvent("event2", properties); -// client.trackEvent("event3", properties, measurements); -// } -// -// public void testTrackTrace() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(); -// LinkedHashMap properties = new LinkedHashMap<>(); -// -// client.trackTrace(null); -// client.trackTrace("trace1"); -// client.trackTrace("trace2", properties); -// } -// -// public void testTrackMetric() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(); -// -// client.trackMetric(null, 0); -// client.trackMetric("metric1", 1.1); -// client.trackMetric("metric2", 3); -// client.trackMetric("metric3", 3.3); -// client.trackMetric("metric3", 4); -// } -// -// public void testTrackException() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(); -// LinkedHashMap properties = new LinkedHashMap<>(); -// LinkedHashMap measurements = new LinkedHashMap<>(); -// -// client.trackHandledException(null); -// client.trackHandledException(new Exception()); -// try { -// throw new InvalidObjectException("this is expected"); -// } catch (InvalidObjectException exception) { -// client.trackHandledException(exception); -// client.trackHandledException(exception, properties); -// } -// } -// -// public void testTrackPageView() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(); -// LinkedHashMap properties = new LinkedHashMap<>(); -// LinkedHashMap measurements = new LinkedHashMap<>(); -// -// client.trackPageView("android page"); -// client.trackPageView("android page"); -// client.trackPageView("android page", properties); -// client.trackPageView("android page", properties, measurements); -// } - - //TODO test at applicable location -// public void testFlush() throws Exception { -// TelemetryClient client = TelemetryClient.getInstance(this.mockActivity); -// client.sendPendingData(); // todo: mock sender and verify that sendPendingData is called -// } -} \ No newline at end of file diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryClientTestE2E.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryClientTestE2E.java deleted file mode 100644 index efede37..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryClientTestE2E.java +++ /dev/null @@ -1,133 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.content.Intent; -import android.test.ActivityUnitTestCase; - -import com.microsoft.applicationinsights.library.config.ApplicationInsightsConfig; - -import java.io.InvalidObjectException; -import java.util.LinkedHashMap; - -public class TelemetryClientTestE2E extends ActivityUnitTestCase { - - public TelemetryClientTestE2E() { - super(MockActivity.class); - } - - private MockTelemetryClient client; - private LinkedHashMap properties; - private LinkedHashMap measurements; - - public void setUp() throws Exception { - super.setUp(); - - Intent intent = new Intent(getInstrumentation().getTargetContext(), MockActivity.class); - this.setActivity(this.startActivity(intent, null, null)); - - MockTelemetryClient.getInstance().mockTrackMethod = false; - ApplicationInsightsConfig config = new ApplicationInsightsConfig(); - Channel.initialize(config); - Channel.getInstance().queue.config.setMaxBatchIntervalMs(20); - - Sender.initialize(config); - - this.properties = new LinkedHashMap(); - this.properties.put("core property", "core value"); - this.measurements = new LinkedHashMap(); - this.measurements.put("core measurement", 5.5); - } - - public void testTrackEvent() throws Exception { - this.client.trackEvent(null); - this.client.trackEvent("event1"); - this.client.trackEvent("event2", properties); - this.client.trackEvent("event3", properties, measurements); - this.validate(); - } - - public void testTrackTrace() throws Exception { - this.client.trackTrace(null); - this.client.trackTrace("trace1"); - this.client.trackTrace("trace2", properties); - this.validate(); - } - - public void testTrackMetric() throws Exception { - this.client.trackMetric(null, 0); - this.client.trackMetric("metric1", 1.1); - this.client.trackMetric("metric2", 3); - this.client.trackMetric("metric3", 3.3); - this.client.trackMetric("metric3", 4); - this.validate(); - } - - public void testTrackException() throws Exception { - this.client.trackHandledException(null); - this.client.trackHandledException(new Exception()); - try { - throw new InvalidObjectException("this is expected"); - } catch (InvalidObjectException exception) { - this.client.trackHandledException(exception); - this.client.trackHandledException(exception, properties); - } - - this.validate(); - } - - public void testTrackPageView() throws Exception { - this.client.trackPageView("android page"); - this.client.trackPageView("android page"); - this.client.trackPageView("android page", properties); - this.client.trackPageView("android page", properties, measurements); - this.validate(); - } - - public void testTrackAllRequests() throws Exception { - Exception exception; - try { - throw new Exception(); - } catch (Exception e) { - exception = e; - } - - Channel.getInstance().queue.config.setMaxBatchCount(10); - for (int i = 0; i < 10; i++) { - this.client.trackEvent("android event"); - this.client.trackTrace("android trace"); - this.client.trackMetric("android metric", 0.0); - this.client.trackHandledException(exception); - this.client.trackPageView("android page"); - Thread.sleep(10); - } - - ApplicationInsights.sendPendingData(); - Thread.sleep(10); - this.validate(); - } - - public void validate() throws Exception { -// try { -// MockQueue queue = MockChannel.getInstance().getQueue(); -// CountDownLatch rspSignal = queue.sender.responseSignal; -// CountDownLatch sendSignal = queue.sender.sendSignal; -// rspSignal.await(30, TimeUnit.SECONDS); -// -// Log.i("RESPONSE", queue.sender.getLastResponse()); -// -// if (rspSignal.getCount() < sendSignal.getCount()) { -// Log.w("BACKEND_ERROR", "response count is lower than enqueue count"); -// } else if (queue.sender.responseCode == 206) { -// Log.w("BACKEND_ERROR", "response is 206, some telemetry was rejected"); -// } -// -// if (queue.sender.responseCode != 200) { -// Assert.fail("response rejected with: " + queue.sender.getLastResponse()); -// } -// -// Assert.assertEquals("response was received", 0, rspSignal.getCount()); -// Assert.assertEquals("queue is empty", 0, queue.getQueueSize()); -// } catch (InterruptedException e) { -// Assert.fail(e.toString()); -// } - } -} \ No newline at end of file diff --git a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryContextTest.java b/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryContextTest.java deleted file mode 100644 index f67add6..0000000 --- a/applicationinsights-android/src/androidTest/java/com/microsoft/applicationinsights/library/TelemetryContextTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.microsoft.applicationinsights.library; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.test.ActivityUnitTestCase; - -import junit.framework.Assert; - -import java.util.Map; -import java.util.UUID; - -public class TelemetryContextTest extends ActivityUnitTestCase { - - public TelemetryContextTest() { - super(com.microsoft.applicationinsights.library.MockActivity.class); - } - - private final String userIdKey = "ai.user.id"; - private final String userAcqKey = "ai.user.accountAcquisitionDate"; - - public void setUp() throws Exception { - super.setUp(); - - Intent intent = new Intent(getInstrumentation().getTargetContext(), com.microsoft.applicationinsights.library.MockActivity.class); - this.setActivity(this.startActivity(intent, null, null)); - - SharedPreferences.Editor editor = this.getActivity().getApplicationContext() - .getSharedPreferences(TelemetryContext.SHARED_PREFERENCES_KEY, 0).edit(); - editor.putString(TelemetryContext.USER_ID_KEY, null); - editor.commit(); - } - - public void tearDown() throws Exception { - - } - - public void testInitialization() { - TelemetryContext telemetryContext = new TelemetryContext(this.getActivity(), "iKey", "1234"); - - Assert.assertNotNull("app", telemetryContext.getApplication()); - Assert.assertNotNull("appVer", telemetryContext.getApplication().getVer()); - Assert.assertNotNull("appPackageName", telemetryContext.getPackageName()); - Assert.assertNotNull("device", telemetryContext.getDevice()); - Assert.assertNotNull("deviceId", telemetryContext.getDevice().getId()); - Assert.assertNotNull("deviceOs", telemetryContext.getDevice().getOs()); - Assert.assertNotNull("user", telemetryContext.getUser()); - Assert.assertNotNull("userId", telemetryContext.getUser().getId()); - Assert.assertNotNull("userAcquisition", telemetryContext.getUser().getAccountAcquisitionDate()); - } - - public void testUserContextInitialization() { - TelemetryContext tc = new PublicTelemetryContext(this.getActivity(), "iKey", "1234"); - - String id = tc.getContextTags().get(userIdKey); - try { - UUID guidId = UUID.fromString(id); - Assert.assertNotNull("generated ID is a valid GUID", guidId); - } catch (Exception e) { - Assert.fail("id was not properly initialized by constructor\n" + e.toString()); - } - } - - public void testUserContextPersistence() { - SharedPreferences.Editor editor = this.getActivity().getApplicationContext() - .getSharedPreferences(TelemetryContext.SHARED_PREFERENCES_KEY, 0).edit(); - editor.putString(TelemetryContext.USER_ID_KEY, "test value"); - editor.putString(TelemetryContext.USER_ACQ_KEY, "test acq"); - editor.commit(); - - // this should load context from shared storage to match firstId - TelemetryContext tc = new PublicTelemetryContext(this.getActivity(), "iKey", "1234"); - Map tags = tc.getContextTags(); - String newId = tags.get(userIdKey); - String newAcq = tags.get(userAcqKey); - Assert.assertEquals("ID persists in local storage", "test value", newId); - Assert.assertEquals("Acquisition date persists in local storage", "test acq", newAcq); - } - - public void testSessionContextInitialization() throws Exception { - TelemetryContext tc = new PublicTelemetryContext(this.getActivity(), "iKey", "1234"); - - String firstId = checkSessionTags(tc); - try { - java.util.UUID.fromString(firstId); - } catch (Exception e) { - Assert.fail("id was not properly initialized by constructor\n" + e.toString()); - } - - // this should load context from shared storage to match firstId - TelemetryContext newerTc = new PublicTelemetryContext(this.getActivity(), "iKey", "1234"); - checkSessionTags(newerTc); - } - - public void testSessionContextRenewal() throws Exception { - TelemetryContext tc = new PublicTelemetryContext(this.getActivity(), "iKey", "1234"); - String firstId = checkSessionTags(tc); - - // trigger renewal - tc.renewSessionId(); - String secondId = checkSessionTags(tc); - Assert.assertNotSame("session id is renewed", firstId, secondId); - - // check that it doesn't change when accessed a second time - String thirdId = checkSessionTags(tc); - Assert.assertSame("session id is not renewed", secondId, thirdId); - } - - private String checkSessionTags(TelemetryContext tc) { - Map tags = tc.getContextTags(); - String sessionIdKey = "ai.session.id"; - return tags.get(sessionIdKey); - } - - private class MockActivity extends Activity { - public Context context; - public MockActivity(Context context) { - this.context = context; - } - - @Override - public Resources getResources() { - return this.context.getResources(); - } - - @Override - public Context getApplicationContext() { - return this.context; - } - - @Override - public String getPackageName() { - return "com.microsoft.applicationinsights.test"; - } - } -} \ No newline at end of file diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/contracts/shared/JsonHelper.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/contracts/shared/JsonHelper.java index 93354f1..8097a23 100755 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/contracts/shared/JsonHelper.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/contracts/shared/JsonHelper.java @@ -1,5 +1,7 @@ package com.microsoft.applicationinsights.contracts.shared; +import com.microsoft.applicationinsights.logging.InternalLogging; + import java.io.IOException; import java.io.Writer; import java.util.Iterator; @@ -227,16 +229,21 @@ public static void writeList(Writer writer, List void writeItem(Writer writer, T item) throws IOException { - if (item instanceof String) { - writer.write(JsonHelper.convert((String) item)); - } else if (item instanceof Double) { - writer.write(JsonHelper.convert((Double) item)); - } else if (item instanceof Integer) { - writer.write(JsonHelper.convert((Integer) item)); - } else if (item instanceof Long) { - writer.write(JsonHelper.convert((Long) item)); - } else { - throw new IOException("Cannot serialize: " + item.toString()); + if(item != null) { + if (item instanceof String) { + writer.write(JsonHelper.convert((String) item)); + } else if (item instanceof Double) { + writer.write(JsonHelper.convert((Double) item)); + } else if (item instanceof Integer) { + writer.write(JsonHelper.convert((Integer) item)); + } else if (item instanceof Long) { + writer.write(JsonHelper.convert((Long) item)); + } else { + throw new IOException("Cannot serialize: " + item.toString()); + } + } + else { + writer.write("null"); } } } diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ApplicationInsights.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ApplicationInsights.java index d02d082..8cdc75c 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ApplicationInsights.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ApplicationInsights.java @@ -32,10 +32,11 @@ public enum ApplicationInsights { private ApplicationInsightsConfig config; /** - * A flag, which determines if auto collection of sessions and page views should be disabled. + * A flag, which determines if auto collection of sessions and page views should be disabled from the start. * Default is false. + * The features can be enabled/disabled at runtime later */ - private boolean autoCollectionDisabled; + private boolean autoLifecycleCollectionDisabled; /** * A flag, which determines if sending telemetry data should be disabled. Default is false. @@ -77,8 +78,15 @@ public enum ApplicationInsights { */ private Map commonProperties; - private static boolean isRunning; - private static boolean isSetup; + /** + * Flag that indicates that the user has called a setup-method before + */ + private static boolean isConfigured; + + /** + * Flag that indicates that the pipeline (Channel, Persistence, etc.) have been setup + */ + private static boolean isSetupAndRunning; /** * Create ApplicationInsights instance @@ -86,24 +94,11 @@ public enum ApplicationInsights { ApplicationInsights() { this.telemetryDisabled = false; this.exceptionTrackingDisabled = false; - this.autoCollectionDisabled = false; + this.autoLifecycleCollectionDisabled = false; this.config = new ApplicationInsightsConfig(); } - /** - * Configure Application Insights - * Note: This should be called before start - * auto-collection of lifecycle-events is disabled when using this method - * - * @param context the context associated with Application Insights - * @param context the application context associated with Application Insights - * @deprecated This method is deprecated: Use setup(Context context, Application application) instead. - */ - public static void setup(Context context) { - ApplicationInsights.INSTANCE.setupInstance(context, null, null); - } - - /** + /** * Configure Application Insights * Note: This should be called before start * @@ -113,19 +108,6 @@ public static void setup(Context context, Application application) { ApplicationInsights.INSTANCE.setupInstance(context, application, null); } - /** - * Configure Application Insights - * Note: This should be called before start - * warning! auto-collection of lifecycle-events is disabled when using this method - * - * @param context the application context associated with Application Insights - * @param instrumentationKey the instrumentation key associated with the app - * @deprecated This method is deprecated: Use setup(Context context, Application application) instead. - */ - public static void setup(Context context, String instrumentationKey) { - ApplicationInsights.INSTANCE.setupInstance(context, null, instrumentationKey); - } - /** * Configure Application Insights * Note: This should be called before start @@ -146,12 +128,12 @@ public static void setup(Context context, Application application, String instru * @param instrumentationKey the instrumentation key associated with the app */ public void setupInstance(Context context, Application application, String instrumentationKey) { - if (!isSetup) { + if (!isConfigured) { if (context != null) { this.weakContext = new WeakReference(context); this.instrumentationKey = instrumentationKey; this.weakApplication = new WeakReference(application); - isSetup = true; + isConfigured = true; InternalLogging.info(TAG, "ApplicationInsights has been setup correctly.", null); } else { InternalLogging.warn(TAG, "ApplicationInsights could not be setup correctly " + @@ -163,7 +145,7 @@ public void setupInstance(Context context, Application application, String instr /** * Start ApplicationInsights - * Note: This should be called after {@link #isSetup} + * Note: This should be called after {@link #isConfigured} */ public static void start() { INSTANCE.startInstance(); @@ -171,15 +153,15 @@ public static void start() { /** * Start ApplicationInsights - * Note: This should be called after {@link #isSetup} + * Note: This should be called after {@link #isConfigured} */ public void startInstance() { - if (!isSetup) { + if (!isConfigured) { InternalLogging.warn(TAG, "Could not start Application Insights since it has not been " + "setup correctly."); return; } - if (!isRunning) { + if (!isSetupAndRunning) { Context context = INSTANCE.getContext(); if (context == null) { @@ -192,36 +174,61 @@ public void startInstance() { } this.telemetryContext = new TelemetryContext(context, this.instrumentationKey, userId); - EnvelopeFactory.initialize(telemetryContext, this.commonProperties); - - Persistence.initialize(context); - Sender.initialize(this.config); - Channel.initialize(this.config); - - // Initialize Telemetry - TelemetryClient.initialize(!telemetryDisabled); - LifeCycleTracking.initialize(telemetryContext, this.config); - Application application = INSTANCE.getApplication(); - LifeCycleTracking.registerForPersistingWhenInBackground(application); - if (INSTANCE.getApplication() != null && !this.autoCollectionDisabled) { - LifeCycleTracking.registerPageViewCallbacks(application); - LifeCycleTracking.registerSessionManagementCallbacks(application); - } else { - InternalLogging.warn(TAG, "Auto collection of page views could not be " + - "started, since the given application was null"); - } - // Start crash reporting - if (!this.exceptionTrackingDisabled) { - ExceptionTracking.registerExceptionHandler(); - } + initializePipeline(context); + startSyncWhenBackgrounding(); + setupAndStartAutocollection(); + startCrashReporting(); - isRunning = true; Sender.getInstance().sendDataOnAppStart(); InternalLogging.info(TAG, "ApplicationInsights has been started.", ""); } } + private void startCrashReporting() { + // Start crash reporting + if (!this.exceptionTrackingDisabled) { + ExceptionTracking.registerExceptionHandler(); + } + } + + private void setupAndStartAutocollection() { + if(INSTANCE.autoLifecycleCollectionDisabled) { + InternalLogging.info(TAG, "Auto collection has been disabled at app start, it can" + + " be enabled using the various enableAuto...-Methods."); + } + else if (autoCollectionPossible("Initialization of AutoCollection at app start")) { + AutoCollection.initialize(telemetryContext, this.config); + enableAutoCollection(); + } + } + + private void startSyncWhenBackgrounding() { + if(!Util.isLifecycleTrackingAvailable()) { + return; + } + + if (INSTANCE.getApplication() != null) { + SyncUtil.getInstance().start(INSTANCE.getApplication()); + } else { + InternalLogging.warn(TAG, "Couldn't turn on SyncUtil because given application " + + "was null"); + } + } + + private void initializePipeline(Context context) { + EnvelopeFactory.initialize(telemetryContext, this.commonProperties); + + Persistence.initialize(context); + Sender.initialize(this.config); + Channel.initialize(this.config); + + // Initialize Telemetry + TelemetryClient.initialize(!telemetryDisabled); + + isSetupAndRunning = true; + } + /** * Triggers persisting and if applicable sending of queued data * note: this will be called @@ -229,7 +236,7 @@ public void startInstance() { * tracking any telemetry so it is not necessary to call this in most cases. */ public static void sendPendingData() { - if (!isRunning) { + if (!isSetupAndRunning) { InternalLogging.warn(TAG, "Could not set send pending data, because " + "ApplicationInsights has not been started, yet."); return; @@ -238,21 +245,23 @@ public static void sendPendingData() { } /** - * Enable auto page view tracking as well as auto session tracking. This will only work, if - * {@link ApplicationInsights#telemetryDisabled} is set to false. + * enables all auto-collection features * - * @param application the application used to register the life cycle callbacks - * @deprecated This method is deprecated: Use setAutoCollectionDisabled instead. + * Requires ApplicationInsights to be setup with an Application object */ - public static void enableActivityTracking(Application application) { - if (!isRunning) { //TODO fix log warning - InternalLogging.warn(TAG, "Could not set activity tracking, because " + - "ApplicationInsights has not been started, yet."); - return; - } - if (!INSTANCE.telemetryDisabled) { - LifeCycleTracking.registerActivityLifecycleCallbacks(application); - } + public static void enableAutoCollection() { + enableAutoAppearanceTracking(); + enableAutoPageViewTracking(); + enableAutoSessionManagement(); + } + + /** + * disables all auto-collection features + */ + public static void disableAutoCollection() { + disableAutoAppearanceTracking(); + disableAutoPageViewTracking(); + disableAutoSessionManagement(); } /** @@ -261,16 +270,8 @@ public static void enableActivityTracking(Application application) { * {@link com.microsoft.applicationinsights.library.ApplicationInsights#start()}. */ public static void enableAutoPageViewTracking() { - if (!isRunning) { - InternalLogging.warn(TAG, "Could not set page view tracking, because " + - "ApplicationInsights has not been started yet."); - return; - } else if (INSTANCE.getApplication() == null) { - InternalLogging.warn(TAG, "Could not set page view tracking, because " + - "ApplicationInsights has not been setup with an application."); - return; - } else { - LifeCycleTracking.registerPageViewCallbacks(INSTANCE.getApplication()); + if(autoCollectionPossible("Auto PageView Tracking")) { + AutoCollection.enableAutoPageViews(INSTANCE.getApplication()); } } @@ -280,16 +281,8 @@ public static void enableAutoPageViewTracking() { * {@link com.microsoft.applicationinsights.library.ApplicationInsights#start()}. */ public static void disableAutoPageViewTracking() { - if (!isRunning) { - InternalLogging.warn(TAG, "Could not unset page view tracking, because " + - "ApplicationInsights has not been started yet."); - return; - } else if (INSTANCE.getApplication() == null) { - InternalLogging.warn(TAG, "Could not unset page view tracking, because " + - "ApplicationInsights has not been setup with an application."); - return; - } else { - LifeCycleTracking.unregisterPageViewCallbacks(INSTANCE.getApplication()); + if(autoCollectionPossible("Auto PageView Tracking")) { + AutoCollection.disableAutoPageViews(); } } @@ -299,16 +292,8 @@ public static void disableAutoPageViewTracking() { * {@link com.microsoft.applicationinsights.library.ApplicationInsights#start()}. */ public static void enableAutoSessionManagement() { - if (!isRunning) { - InternalLogging.warn(TAG, "Could not set session management, because " + - "ApplicationInsights has not been started yet."); - return; - } else if (INSTANCE.getApplication() == null) { - InternalLogging.warn(TAG, "Could not set session management, because " + - "ApplicationInsights has not been setup with an application."); - return; - } else { - LifeCycleTracking.registerSessionManagementCallbacks(INSTANCE.getApplication()); + if(autoCollectionPossible("Auto Session Management")) { + AutoCollection.enableAutoSessionManagement(INSTANCE.getApplication()); } } @@ -318,16 +303,51 @@ public static void enableAutoSessionManagement() { * {@link com.microsoft.applicationinsights.library.ApplicationInsights#start()}. */ public static void disableAutoSessionManagement() { - if (!isRunning) { - InternalLogging.warn(TAG, "Could not unset session management, because " + + if(autoCollectionPossible("Auto Session Management")) { + AutoCollection.disableAutoSessionManagement(); + } + } + + /** + * Enable auto appearance tracking. This will only work, if ApplicationInsights has been setup + * with an application. This method should only be called after + * {@link com.microsoft.applicationinsights.library.ApplicationInsights#start()}. + */ + public static void enableAutoAppearanceTracking() { + if(autoCollectionPossible("Auto Appearance")) { + AutoCollection.enableAutoAppearanceTracking(INSTANCE.getApplication()); + } + } + + /** + * Disable auto appearance tracking. This will only work, if ApplicationInsights has been setup + * with an application. This method should only be called after + * {@link com.microsoft.applicationinsights.library.ApplicationInsights#start()}. + */ + public static void disableAutoAppearanceTracking() { + if(autoCollectionPossible("Auto Appearance")) { + AutoCollection.disableAutoAppearanceTracking(); + } + } + + private static boolean autoCollectionPossible(String featureName) { + if (!Util.isLifecycleTrackingAvailable()) { + InternalLogging.warn(TAG, "AutoCollection feature " + featureName + + " can't be enabled/disabled, because " + + "it is not supported on this OS version."); + return false; + } else if (!isSetupAndRunning) { + InternalLogging.warn(TAG, "AutoCollection feature " + featureName + + " can't be enabled/disabled, because " + "ApplicationInsights has not been started yet."); - return; + return false; } else if (INSTANCE.getApplication() == null) { - InternalLogging.warn(TAG, "Could not unset session management, because " + + InternalLogging.warn(TAG, "AutoCollection feature " + featureName + + " can't be enabled/disabled, because " + "ApplicationInsights has not been setup with an application."); - return; + return false; } else { - LifeCycleTracking.unregisterSessionManagementCallbacks(INSTANCE.getApplication()); + return true; } } @@ -337,12 +357,12 @@ public static void disableAutoSessionManagement() { * @param disabled if set to true, crash reporting will be disabled */ public static void setExceptionTrackingDisabled(boolean disabled) { - if (!isSetup) { + if (!isConfigured) { InternalLogging.warn(TAG, "Could not enable/disable exception tracking, because " + "ApplicationInsights has not been setup correctly."); return; } - if (isRunning) { + if (isSetupAndRunning) { InternalLogging.warn(TAG, "Could not enable/disable exception tracking, because " + "ApplicationInsights has already been started."); return; @@ -356,12 +376,12 @@ public static void setExceptionTrackingDisabled(boolean disabled) { * @param disabled if set to true, the telemetry feature will be disabled */ public static void setTelemetryDisabled(boolean disabled) { - if (!isSetup) { + if (!isConfigured) { InternalLogging.warn(TAG, "Could not enable/disable telemetry, because " + "ApplicationInsights has not been setup correctly."); return; } - if (isRunning) { + if (isSetupAndRunning) { InternalLogging.warn(TAG, "Could not enable/disable telemetry, because " + "ApplicationInsights has already been started."); return; @@ -373,19 +393,50 @@ public static void setTelemetryDisabled(boolean disabled) { * Enable / disable auto collection of telemetry data. * * @param disabled if set to true, the auto collection feature will be disabled + * @deprecated with 1.0-beta.5 + * To enable/disable at runtime, use {@link ApplicationInsights#disableAutoCollection()} or the more specific + * {@link ApplicationInsights#disableAutoSessionManagement()}, + * {@link ApplicationInsights#disableAutoAppearanceTracking()} and + * {@link ApplicationInsights#disableAutoPageViewTracking()} + * */ public static void setAutoCollectionDisabled(boolean disabled) { - if (!isSetup) { + if (!isConfigured) { + InternalLogging.warn(TAG, "Could not enable/disable auto collection, because " + + "ApplicationInsights has not been setup correctly."); + return; + } + if (isSetupAndRunning) { + InternalLogging.warn(TAG, "Could not enable/disable auto collection, because " + + "ApplicationInsights has already been started."); + return; + } + INSTANCE.autoLifecycleCollectionDisabled = disabled; + } + + /** + * Enable / disable auto collection of telemetry data at startup. + * + * @param disabled if set to true, the auto collection feature will be disabled at app start + * To enable/disable auto collection features at runtime, use + * {@link ApplicationInsights#disableAutoCollection()} or the more specific + * {@link ApplicationInsights#disableAutoSessionManagement()}, + * {@link ApplicationInsights#disableAutoAppearanceTracking()} and + * {@link ApplicationInsights#disableAutoPageViewTracking()} + * + */ + public static void setAutoCollectionDisabledAtStartup(boolean disabled) { + if (!isConfigured) { InternalLogging.warn(TAG, "Could not enable/disable auto collection, because " + "ApplicationInsights has not been setup correctly."); return; } - if (isRunning) { + if (isSetupAndRunning) { InternalLogging.warn(TAG, "Could not enable/disable auto collection, because " + "ApplicationInsights has already been started."); return; } - INSTANCE.autoCollectionDisabled = disabled; + INSTANCE.autoLifecycleCollectionDisabled = disabled; } /** @@ -403,12 +454,12 @@ public static Map getCommonProperties() { * @param commonProperties a dictionary of properties to enqueue with all telemetry. */ public static void setCommonProperties(Map commonProperties) { - if (!isSetup) { + if (!isConfigured) { InternalLogging.warn(TAG, "Could not set common properties, because " + "ApplicationInsights has not been setup correctly."); return; } - if (isRunning) { + if (isSetupAndRunning) { InternalLogging.warn(TAG, "Could not set common properties, because " + "ApplicationInsights has already been started."); return; @@ -505,12 +556,12 @@ public static ApplicationInsightsConfig getConfig() { * Sets the session configuration for the instance */ public void setConfig(ApplicationInsightsConfig config) { - if (!isSetup) { + if (!isConfigured) { InternalLogging.warn(TAG, "Could not set telemetry configuration, because " + "ApplicationInsights has not been setup correctly."); return; } - if (isRunning) { + if (isSetupAndRunning) { InternalLogging.warn(TAG, "Could not set telemetry configuration, because " + "ApplicationInsights has already been started."); return; @@ -536,7 +587,7 @@ public static void renewSession(String sessionId) { * @param userId a user ID associated with the telemetry data */ public static void setUserId(String userId) { - if (isRunning) { + if (isSetupAndRunning) { INSTANCE.telemetryContext.configUserContext(userId); } else { INSTANCE.userId = userId; diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/AutoCollection.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/AutoCollection.java new file mode 100644 index 0000000..2f45366 --- /dev/null +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/AutoCollection.java @@ -0,0 +1,384 @@ +package com.microsoft.applicationinsights.library; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Application; +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; + +import com.microsoft.applicationinsights.library.config.ISessionConfig; +import com.microsoft.applicationinsights.logging.InternalLogging; + +import java.util.Date; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +class AutoCollection implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 { + /** + * The activity counter + */ + protected final AtomicInteger activityCount; + + /** + * The configuration for tracking sessions + */ + protected ISessionConfig config; + + /** + * The timestamp of the last activity + */ + protected final AtomicLong lastBackground; + + /** + * The telemetryContext which is needed to renew a session + */ + protected TelemetryContext telemetryContext; + + /** + * Volatile boolean for double checked synchronize block + */ + private static volatile boolean isLoaded = false; + + /** + * Synchronization LOCK for setting static context + */ + private static final Object LOCK = new Object(); + + /** + * The singleton INSTANCE of this class + */ + private static AutoCollection instance; + + /** + * The tag for logging + */ + private static final String TAG = "AutoCollection"; + + /** + * A flag which determines whether auto page view tracking has been enabled or not. + */ + private static boolean autoPageViewsEnabled; + + /** + * A flag which determines whether session management has been enabled or not. + */ + private static boolean autoSessionManagementEnabled; + ; + + protected static boolean isAutoAppearanceTrackingEnabled() { + return autoAppearanceTrackingEnabled; + } + + protected static boolean isHasRegisteredComponentCallbacks() { + return hasRegisteredComponentCallbacks; + } + + protected static boolean isHasRegisteredLifecycleCallbacks() { + return hasRegisteredLifecycleCallbacks; + } + + protected static boolean isAutoPageViewsEnabled() { + return autoPageViewsEnabled; + } + + protected static boolean isAutoSessionManagementEnabled() { + return autoSessionManagementEnabled; + } + + /** + * A flag that determines whether we want to auto-track events for foregrounding backgrounding + */ + private static boolean autoAppearanceTrackingEnabled; + + /** + * A flag that indicates if componentcallbacks have been registered + */ + private static boolean hasRegisteredComponentCallbacks; + + /** + * A flag that indicates if lifecyclecallbacks have been already registered + */ + private static boolean hasRegisteredLifecycleCallbacks; + + /** + * Create a new INSTANCE of the autocollection event tracking + * + * @param config the session configuration for session tracking + * @param telemetryContext the context, which is needed to renew sessions + */ + protected AutoCollection(ISessionConfig config, TelemetryContext telemetryContext) { + this.activityCount = new AtomicInteger(0); + this.lastBackground = new AtomicLong(this.getTime()); + this.config = config; + this.telemetryContext = telemetryContext; + } + + /** + * Initialize the INSTANCE of Autocollection event tracking. + * + * @param telemetryContext the context, which is needed to renew sessions + * @param config the session configuration for session tracking + */ + protected static void initialize(TelemetryContext telemetryContext, ISessionConfig config) { + // note: isLoaded must be volatile for the double-checked LOCK to work + if (!AutoCollection.isLoaded) { + synchronized (AutoCollection.LOCK) { + if (!AutoCollection.isLoaded) { + AutoCollection.isLoaded = true; + AutoCollection.hasRegisteredComponentCallbacks = false; + AutoCollection.hasRegisteredLifecycleCallbacks = false; + AutoCollection.instance = new AutoCollection(config, telemetryContext); + } + } + } + } + + /** + * @return the INSTANCE of autocollection event tracking or null if not yet initialized + */ + protected static AutoCollection getInstance() { + if (AutoCollection.instance == null) { + InternalLogging.error(TAG, "getInstance was called before initialization"); + } + + return AutoCollection.instance; + } + + /** + * Enables lifecycle event tracking for the provided application + * + * @param application the application object + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private static void registerActivityLifecycleCallbacks(Application application) { + if (!hasRegisteredLifecycleCallbacks) { + if ((application != null ) && Util.isLifecycleTrackingAvailable()) { + application.registerActivityLifecycleCallbacks(AutoCollection.getInstance()); + hasRegisteredLifecycleCallbacks = true; + InternalLogging.info(TAG, "Registered activity lifecycle callbacks"); + } + } + } + + /** + * Register for component callbacks to enable persisting when backgrounding on devices with API-level 14+ + * and persisting when receiving onMemoryLow() on devices with API-level 1+ + * + * @param application the application object + */ + private static void registerForComponentCallbacks(Application application) { + if (!hasRegisteredComponentCallbacks) { + if ((application != null ) && Util.isLifecycleTrackingAvailable()) { + application.registerComponentCallbacks(AutoCollection.getInstance()); + hasRegisteredComponentCallbacks = true; + InternalLogging.info(TAG, "Registered component callbacks"); + } + } + } + + /** + * Enables page view event tracking for the provided application + * + * @param application the application object + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected static void enableAutoPageViews(Application application) { + if (application != null && Util.isLifecycleTrackingAvailable()) { + synchronized (AutoCollection.LOCK) { + registerActivityLifecycleCallbacks(application); + autoPageViewsEnabled = true; + } + } + } + + /** + * Disables page view event tracking for the provided application* + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected static void disableAutoPageViews() { + if (Util.isLifecycleTrackingAvailable()) { + synchronized (AutoCollection.LOCK) { + autoPageViewsEnabled = false; + } + } + } + + /** + * Enables session event tracking for the provided application + * + * @param application the application object + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected static void enableAutoSessionManagement(Application application) { + if (application != null && Util.isLifecycleTrackingAvailable()) { + synchronized (AutoCollection.LOCK) { + registerForComponentCallbacks(application); + registerActivityLifecycleCallbacks(application); + autoSessionManagementEnabled = true; + } + } + } + + /** + * Disables session event tracking for the provided application + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected static void disableAutoSessionManagement() { + if (Util.isLifecycleTrackingAvailable()) { + synchronized (AutoCollection.LOCK) { + autoSessionManagementEnabled = false; + } + } + } + + /** + * Enables auto appearance event tracking for the provided application + * + * @param application the application object + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected static void enableAutoAppearanceTracking(Application application) { + if (application != null && Util.isLifecycleTrackingAvailable()) { + synchronized (AutoCollection.LOCK) { + registerForComponentCallbacks(application); + registerActivityLifecycleCallbacks(application); + autoAppearanceTrackingEnabled = true; + } + } + } + + + /** + * Disables auto appearance event tracking for the provided application + */ + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + protected static void disableAutoAppearanceTracking() { + if (Util.isLifecycleTrackingAvailable()) { + synchronized (AutoCollection.LOCK) { + autoAppearanceTrackingEnabled = false; + } + } + } + + /** + * This is called each time an activity is created. + * + * @param activity the Android Activity that's created + * @param savedInstanceState the bundle + */ + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + int count = this.activityCount.getAndIncrement(); + synchronized (AutoCollection.LOCK) { + if (count == 0) { + if (autoSessionManagementEnabled) { + InternalLogging.info(TAG, "Starting & tracking session"); + TrackDataOperation sessionOp = new TrackDataOperation(TrackDataOperation.DataType.NEW_SESSION); + new Thread(sessionOp).start(); + } + if(autoAppearanceTrackingEnabled) { + //TODO track cold start as soon as it's available in new Schema. + } + } + } + } + + /** + * This is called each time an activity becomes visible + * + * @param activity the activity which entered the foreground + */ + public void onActivityStarted(Activity activity) { + // unused but required to implement ActivityLifecycleCallbacks + } + + /** + * This is called each time an activity has been started or was resumed after pause + * + * @param activity the activity which left the foreground + */ + public void onActivityResumed(Activity activity) { + // check if the session should be renewed + long now = this.getTime(); + long then = this.lastBackground.getAndSet(this.getTime()); + boolean shouldRenew = ((now - then) >= this.config.getSessionIntervalMs()); + + synchronized (AutoCollection.LOCK) { + if (autoSessionManagementEnabled && shouldRenew) { + InternalLogging.info(TAG, "Renewing session"); + this.telemetryContext.renewSessionId(); + TrackDataOperation sessionOp = new TrackDataOperation(TrackDataOperation.DataType.NEW_SESSION); + new Thread(sessionOp).start(); + } + + if (autoPageViewsEnabled) { + InternalLogging.info(TAG, "New Pageview"); + TrackDataOperation pageViewOp = new TrackDataOperation(TrackDataOperation.DataType.PAGE_VIEW, activity.getClass().getName(), null, null); + new Thread(pageViewOp).start(); + } + } + } + + /** + * This is called each time an activity leaves the foreground + * + * @param activity the activity which was paused + */ + public void onActivityPaused(Activity activity) { + //set backgrounding in onTrimMemory + } + + public void onActivityStopped(Activity activity) { + // unused but required to implement ActivityLifecycleCallbacks + } + + public void onActivitySaveInstanceState(Activity activity, Bundle outState) { + // unused but required to implement ActivityLifecycleCallbacks + } + + public void onActivityDestroyed(Activity activity) { + // unused but required to implement ActivityLifecycleCallbacks + } + + @Override + public void onTrimMemory(int level) { + if (level == TRIM_MEMORY_UI_HIDDEN) { + InternalLogging.info(TAG, "UI of the app is hidden"); + InternalLogging.info(TAG, "Setting background time"); + this.lastBackground.set(this.getTime()); + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + switch (newConfig.orientation) { + case Configuration.ORIENTATION_PORTRAIT: + InternalLogging.info(TAG, "Device Orientation is portrait"); + break; + case Configuration.ORIENTATION_LANDSCAPE: + InternalLogging.info(TAG, "Device Orientation is landscape"); + break; + case Configuration.ORIENTATION_UNDEFINED: + InternalLogging.info(TAG, "Device Orientation is undefinded"); + break; + default: + break; + } + } + + @Override + public void onLowMemory() { + // unused but required to implement ComponentCallbacks + } + + /** + * Test hook to get the current time + * + * @return the current time in milliseconds + */ + protected long getTime() { + return new Date().getTime(); + } +} diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Channel.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Channel.java index 4e85695..8561de8 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Channel.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Channel.java @@ -1,10 +1,13 @@ package com.microsoft.applicationinsights.library; import com.microsoft.applicationinsights.contracts.Envelope; -import com.microsoft.applicationinsights.contracts.shared.IJsonSerializable; +import com.microsoft.applicationinsights.contracts.Internal; import com.microsoft.applicationinsights.library.config.IQueueConfig; import com.microsoft.applicationinsights.logging.InternalLogging; +import java.io.IOException; +import java.io.StringWriter; + /** * This class records telemetry for application insights. */ @@ -72,6 +75,9 @@ protected static Channel getInstance() { */ protected void synchronize() { this.queue.flush(); + if(Sender.getInstance() != null) { + Sender.getInstance().sendNextFile(); + } } /** @@ -80,23 +86,40 @@ protected void synchronize() { * @param envelope the envelope object to record */ protected void enqueue(Envelope envelope) { + String serializedData = this.serializeEnvelope(envelope); + // enqueue to queue - queue.enqueue(envelope); + queue.enqueue(serializedData); InternalLogging.info(TAG, "enqueued telemetry", envelope.getName()); } + protected String serializeEnvelope(Envelope envelope) { + try { + if (envelope != null) { + StringWriter stringWriter = new StringWriter(); + envelope.serialize(stringWriter); + return stringWriter.toString(); + } + InternalLogging.warn(TAG, "Envelop wasn't empty but failed to serialize anything, returning null"); + return null; + } catch (IOException e) { + InternalLogging.warn(TAG, "Failed to save data with exception: " + e.toString()); + return null; + } + } + protected void processUnhandledException(Envelope envelope) { queue.isCrashing = true; queue.flush(); - IJsonSerializable[] data = new IJsonSerializable[1]; - data[0] = envelope; + String[] data = new String[1]; + data[0] = serializeEnvelope(envelope); if (this.persistence != null) { + InternalLogging.info(TAG, "persisting crash", envelope.toString()); this.persistence.persist(data, true); - } - else { + } else { InternalLogging.info(TAG, "error persisting crash", envelope.toString()); } diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ChannelQueue.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ChannelQueue.java index 5a751a1..a86b281 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ChannelQueue.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ChannelQueue.java @@ -1,6 +1,5 @@ package com.microsoft.applicationinsights.library; -import com.microsoft.applicationinsights.contracts.shared.IJsonSerializable; import com.microsoft.applicationinsights.library.config.IQueueConfig; import com.microsoft.applicationinsights.logging.InternalLogging; @@ -37,7 +36,7 @@ class ChannelQueue { /** * The linked list for this queue */ - protected final List list; + protected final List list; /** * If true the app is crashing and data should be persisted instead of sent @@ -58,7 +57,7 @@ class ChannelQueue { * Prevent external instantiation */ protected ChannelQueue(IQueueConfig config) { - this.list = new LinkedList(); + this.list = new LinkedList(); this.timer = new Timer("Application Insights Sender Queue", true); this.config = config; this.isCrashing = false; @@ -68,19 +67,19 @@ protected ChannelQueue(IQueueConfig config) { /** * Adds an item to the sender queue * - * @param item a telemetry item to enqueue + * @param serializedItem a serialized telemetry item to enqueue * @return true if the item was successfully added to the queue */ - protected boolean enqueue(IJsonSerializable item) { + protected boolean enqueue(String serializedItem) { // prevent invalid argument exception - if (item == null) { + if (serializedItem == null) { return false; } boolean success; synchronized (this.LOCK) { // attempt to add the item to the queue - success = this.list.add(item); + success = this.list.add(serializedItem); if (success) { if ((this.list.size() >= this.config.getMaxBatchCount()) || isCrashing) { @@ -106,10 +105,10 @@ protected void flush() { this.scheduledPersistenceTask.cancel(); } - IJsonSerializable[] data; + String[] data; synchronized (this.LOCK) { if (!list.isEmpty()) { - data = new IJsonSerializable[list.size()]; + data = new String[list.size()]; list.toArray(data); list.clear(); @@ -132,7 +131,7 @@ protected void schedulePersitenceTask(){ /** * Initiates persisting the content queue. */ - protected void executePersistenceTask(IJsonSerializable[] data){ + protected void executePersistenceTask(String[] data){ if (data != null) { if (persistence != null) { persistence.persist(data, false); diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ExceptionTracking.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ExceptionTracking.java index ff6d735..cd84965 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ExceptionTracking.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/ExceptionTracking.java @@ -53,6 +53,8 @@ protected static void registerExceptionHandler(boolean ignoreDefaultHandler) { ignoreDefaultHandler); Thread.setDefaultUncaughtExceptionHandler(handler); + InternalLogging.info(TAG, + "ExceptionHandler was registered successfully", ""); } } } diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/LifeCycleTracking.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/LifeCycleTracking.java index db78446..014a504 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/LifeCycleTracking.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/LifeCycleTracking.java @@ -17,7 +17,10 @@ /** * The public API for auto collecting application insights telemetry. + * @warning Deprecated with 1.0-beta.5, please use + * {@link com.microsoft.applicationinsights.library.AutoCollection} instead */ +@Deprecated @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) class LifeCycleTracking implements Application.ActivityLifecycleCallbacks, ComponentCallbacks2 { @@ -149,7 +152,7 @@ private static void unregisterActivityLifecycleCallbacks(Application application */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public static void registerPageViewCallbacks(Application application) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (application != null && Util.isLifecycleTrackingAvailable()) { synchronized (LifeCycleTracking.LOCK) { registerActivityLifecycleCallbacks(application); autoPageViewsEnabled = true; @@ -164,9 +167,10 @@ public static void registerPageViewCallbacks(Application application) { * @param application the application object */ public static void registerForPersistingWhenInBackground(Application application) { - application.unregisterComponentCallbacks(LifeCycleTracking.getInstance()); - application.registerComponentCallbacks(LifeCycleTracking.getInstance()); - InternalLogging.warn(TAG, "Registered component callbacks"); + if(application != null){ + application.registerComponentCallbacks(LifeCycleTracking.getInstance()); + InternalLogging.warn(TAG, "Registered component callbacks"); + } } /** @@ -176,7 +180,7 @@ public static void registerForPersistingWhenInBackground(Application application */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public static void unregisterPageViewCallbacks(Application application) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (application != null && Util.isLifecycleTrackingAvailable()) { synchronized (LifeCycleTracking.LOCK) { unregisterActivityLifecycleCallbacks(application); autoPageViewsEnabled = false; @@ -191,7 +195,7 @@ public static void unregisterPageViewCallbacks(Application application) { */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public static void registerSessionManagementCallbacks(Application application) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (application != null && Util.isLifecycleTrackingAvailable()) { synchronized (LifeCycleTracking.LOCK) { registerActivityLifecycleCallbacks(application); autoSessionManagementEnabled = true; @@ -206,7 +210,7 @@ public static void registerSessionManagementCallbacks(Application application) { */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public static void unregisterSessionManagementCallbacks(Application application) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (application != null && Util.isLifecycleTrackingAvailable()) { synchronized (LifeCycleTracking.LOCK) { unregisterActivityLifecycleCallbacks(application); autoSessionManagementEnabled = false; diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Persistence.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Persistence.java index 546e458..59161a7 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Persistence.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Persistence.java @@ -2,16 +2,13 @@ import android.content.Context; -import com.microsoft.applicationinsights.contracts.shared.IJsonSerializable; import com.microsoft.applicationinsights.logging.InternalLogging; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStreamReader; -import java.io.StringWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.UUID; @@ -95,11 +92,11 @@ protected static Persistence getInstance() { /** * Serializes a IJsonSerializable[] and calls: * - * @param data the data to serialize and save to disk + * @param data the data to save to disk * @param highPriority the priority to save the data with - * @see Persistence#persist(String, Boolean) + * @see Persistence#writeToDisk(String, Boolean) */ - protected void persist(IJsonSerializable[] data, Boolean highPriority) { + protected void persist(String[] data, Boolean highPriority) { if (!this.isFreeSpaceAvailable(highPriority)) { InternalLogging.warn(TAG, "No free space on disk to flush data."); Sender.getInstance().sendNextFile(); @@ -108,28 +105,22 @@ protected void persist(IJsonSerializable[] data, Boolean highPriority) { StringBuilder buffer = new StringBuilder(); Boolean isSuccess; - try { - buffer.append('['); - for (int i = 0; i < data.length; i++) { - if (i > 0) { - buffer.append(','); - } - StringWriter stringWriter = new StringWriter(); - data[i].serialize(stringWriter); - buffer.append(stringWriter.toString()); + buffer.append('['); + for (int i = 0; i < data.length; i++) { + if (i > 0) { + buffer.append(','); } + buffer.append(data[i]); + } - buffer.append(']'); - String serializedData = buffer.toString(); - isSuccess = this.persist(serializedData, highPriority); - if (isSuccess) { - Sender sender = Sender.getInstance(); - if (sender != null) { - sender.sendNextFile(); - } + buffer.append(']'); + String serializedData = buffer.toString(); + isSuccess = this.writeToDisk(serializedData, highPriority); + if (isSuccess) { + Sender sender = Sender.getInstance(); + if (sender != null && !highPriority) { + sender.sendNextFile(); } - } catch (IOException e) { - InternalLogging.warn(TAG, "Failed to save data with exception: " + e.toString()); } } @@ -140,7 +131,7 @@ protected void persist(IJsonSerializable[] data, Boolean highPriority) { * @param highPriority the priority we want to use for persisting the data * @return true if the operation was successful, false otherwise */ - protected boolean persist(String data, Boolean highPriority) { + protected boolean writeToDisk(String data, Boolean highPriority) { String uuid = UUID.randomUUID().toString(); Boolean isSuccess = false; Context context = this.getContext(); @@ -151,13 +142,17 @@ protected boolean persist(String data, Boolean highPriority) { if (highPriority) { filesDir = new File(filesDir + AI_SDK_DIRECTORY + HIGH_PRIO_DIRECTORY + uuid); outputStream = new FileOutputStream(filesDir, true); + InternalLogging.warn(TAG, "Saving data" + "HIGH PRIO"); } else { filesDir = new File(filesDir + AI_SDK_DIRECTORY + REGULAR_PRIO_DIRECTORY + uuid); outputStream = new FileOutputStream(filesDir, true); + InternalLogging.warn(TAG, "Saving data" + "REGULAR PRIO"); } outputStream.write(data.getBytes()); outputStream.close(); isSuccess = true; + InternalLogging.warn(TAG, "Saved data"); + } catch (Exception e) { //Do nothing InternalLogging.warn(TAG, "Failed to save data with exception: " + e.toString()); @@ -202,12 +197,16 @@ protected String load(File file) { * @return the next available file. */ protected File nextAvailableFile() { - File file = this.nextHighPrioFile(); - if (file != null) { - return file; - } else { - return this.nextRegularPrioFile(); + synchronized (Persistence.LOCK) { + File file = this.nextHighPrioFile(); + if (file != null) { + return file; + } else { + InternalLogging.info(TAG, "High prio file was empty", "(That's the default if no crashes present"); + return this.nextRegularPrioFile(); + } } + } @@ -216,6 +215,8 @@ private File nextHighPrioFile() { if (context != null) { String path = context.getFilesDir() + AI_SDK_DIRECTORY + HIGH_PRIO_DIRECTORY; File directory = new File(path); + InternalLogging.info(TAG, "Returning High Prio File: ", path); + return this.nextAvailableFileInDirectory(directory); } @@ -229,6 +230,7 @@ private File nextRegularPrioFile() { if (context != null) { String path = context.getFilesDir() + AI_SDK_DIRECTORY + REGULAR_PRIO_DIRECTORY; File directory = new File(path); + InternalLogging.info(TAG, "Returning Regular Prio File: " + path); return this.nextAvailableFileInDirectory(directory); } @@ -245,18 +247,29 @@ private File nextAvailableFileInDirectory(File directory) { if (directory != null) { File[] files = directory.listFiles(); File file; + if ((files != null) && (files.length > 0)) { - for (int i = 0; i < files.length - 1; i++) { + for (int i = 0; i <= files.length - 1; i++) { + InternalLogging.info(TAG, "The directory " + directory.toString(), " ITERATING over " + files.length + " files" ); + file = files[i]; + InternalLogging.info(TAG, "The directory " +file.toString(), " FOUND" ); + if (!this.servedFiles.contains(file)) { + InternalLogging.info(TAG, "The directory " + file.toString(), " ADDING TO SERVED AND RETURN" ); + this.servedFiles.add(file); return file;//we haven't served the file, return it } + else { + InternalLogging.info(TAG, "The directory " + file.toString(), " WAS ALREADY SERVED" ); + } } - } - } + InternalLogging.info(TAG, "The directory " + directory.toString(), " NO FILES" ); + } + InternalLogging.info(TAG, "The directory " + directory.toString(), "Did not contain any unserved files" ); return null; //no files in directory or no directory } } @@ -274,6 +287,7 @@ protected void deleteFile(File file) { if (!deletedFile) { InternalLogging.warn(TAG, "Error deleting telemetry file " + file.toString()); } else { + InternalLogging.info(TAG, "Successfully deleted telemetry file ", file.toString()); servedFiles.remove(file); } } @@ -321,7 +335,7 @@ private void createDirectoriesIfNecessary() { String filesDirPath = getContext().getFilesDir().getPath(); //create high prio directory File dir = new File(filesDirPath + AI_SDK_DIRECTORY + HIGH_PRIO_DIRECTORY); - String successMessage = "Successfully created regular directory"; + String successMessage = "Successfully created directory"; String errorMessage = "Error creating directory"; if (!dir.exists()) { if (dir.mkdirs()) { diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Sender.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Sender.java index 3e4704b..b005ca5 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Sender.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Sender.java @@ -129,9 +129,7 @@ protected void sendNextFile() { protected void send(File fileToSend) { String persistedData = this.persistence.load(fileToSend); if (!persistedData.isEmpty()) { - InternalLogging.info(TAG, "sending persisted data", persistedData); try { - InternalLogging.info(TAG, "sending persisted data", persistedData); this.operationsCount.getAndIncrement(); this.sendRequestWithPayload(persistedData, fileToSend); } catch (IOException e) { @@ -161,7 +159,7 @@ protected void sendRequestWithPayload(String payload, File fileToSend) throws IO connection.setUseCaches(false); try { - InternalLogging.info(TAG, "writing payload", payload); + InternalLogging.info(TAG, "Logging payload", payload); writer = getWriter(connection); writer.write(payload); writer.flush(); diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/SyncUtil.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/SyncUtil.java new file mode 100644 index 0000000..6eac217 --- /dev/null +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/SyncUtil.java @@ -0,0 +1,73 @@ +package com.microsoft.applicationinsights.library; + +import android.annotation.TargetApi; +import android.app.Application; +import android.content.ComponentCallbacks2; +import android.content.res.Configuration; +import android.os.Build; + +import com.microsoft.applicationinsights.logging.InternalLogging; + +/** + * Class that triggers a sync call to the pipeline by using ComponentCallbacks2 + */ +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +class SyncUtil implements ComponentCallbacks2 { + + /** + * The singleton INSTANCE of this class + */ + private static SyncUtil instance; + + /** + * The tag for logging + */ + private static final String TAG = "SyncUtil"; + + /** + * @return the INSTANCE of autocollection event tracking or null if not yet initialized + */ + protected static SyncUtil getInstance() { + if (SyncUtil.instance == null) { + SyncUtil.instance = new SyncUtil(); + } + + return SyncUtil.instance; + } + + + private SyncUtil() { + } + + protected void start(Application application) { + if (application != null) { + application.registerComponentCallbacks(SyncUtil.instance); + InternalLogging.info(TAG, "Started listening to componentcallbacks to trigger sync"); + } + } + + public void onTrimMemory(int level) { + if (Util.isLifecycleTrackingAvailable()) { + if (level == TRIM_MEMORY_UI_HIDDEN) { + InternalLogging.info(TAG, "UI of the app is hidden"); + InternalLogging.info(TAG, "Syncing data"); + Channel.getInstance().synchronize(); + } else if (level == TRIM_MEMORY_RUNNING_LOW || level == TRIM_MEMORY_RUNNING_LOW) { + InternalLogging.info(TAG, "Memory running low, syncing data"); + Channel.getInstance().synchronize(); + } + } + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // unused but required to implement ComponentCallbacks + } + + @Override + public void onLowMemory() { + // unused but required to implement ComponentCallbacks + InternalLogging.warn(TAG, "Received onLowMemory()-Callback, persisting data"); + Channel.getInstance().synchronize(); + } +} diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TelemetryClient.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TelemetryClient.java index e426626..4bc4fc8 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TelemetryClient.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TelemetryClient.java @@ -134,7 +134,6 @@ public void trackEvent( this.executorService.execute(new TrackDataOperation(TrackDataOperation.DataType.EVENT, eventName, properties, measurements)); } - } /** diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TrackDataOperation.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TrackDataOperation.java index fe29c61..d4f328b 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TrackDataOperation.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/TrackDataOperation.java @@ -3,6 +3,12 @@ import com.microsoft.applicationinsights.contracts.Envelope; import com.microsoft.applicationinsights.contracts.shared.ITelemetry; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.HashMap; import java.util.Map; class TrackDataOperation implements Runnable { @@ -28,35 +34,63 @@ protected enum DataType { protected TrackDataOperation(ITelemetry telemetry) { this.type = DataType.NONE; - this.telemetry = telemetry; + try { + this.telemetry = (ITelemetry)deepCopy(telemetry); + } + catch (Exception e) { + e.printStackTrace(); + } } protected TrackDataOperation(DataType type) { - this.type = type; + this.type = type; // no need to copy as enum is pass by value } protected TrackDataOperation(DataType type, String metricName, double metric) { - this.type = type; - this.name = metricName; - this.metric = metric; + this.type = type; // no need to copy as enum is pass by value + this.metric = metric; // no need to copy as enum is pass by value + try { + this.name = (String) deepCopy(metricName); + } + catch (Exception e) { + e.printStackTrace(); + } } protected TrackDataOperation(DataType type, String name, Map properties, Map measurements) { - this.type = type; - this.name = name; - this.properties = properties; - this.measurements = measurements; + this.type = type; // no need to copy as enum is pass by value + try { + this.name = (String) deepCopy(name); + if(properties != null) { + this.properties = new HashMap(properties); + } + if(measurements != null) { + this.measurements = new HashMap(measurements); + } + } + catch (Exception e) { + e.printStackTrace(); + } + } protected TrackDataOperation(DataType type, Throwable exception, Map properties) { - this.type = type; - this.exception = exception; - this.properties = properties; + this.type = type; // no need to copy as enum is pass by value + try { + this.exception = (Throwable) deepCopy(exception); + if(properties != null) { + this.properties = new HashMap(properties); + } + } + catch (Exception e) { + e.printStackTrace(); + } + } @Override @@ -87,7 +121,7 @@ public void run() { envelope = EnvelopeFactory.getInstance().createNewSessionEnvelope(); break; case HANDLED_EXCEPTION: - case UNHANDLED_EXCEPTION: + envelope = EnvelopeFactory.getInstance().createExceptionEnvelope(this.exception, this.properties); break; default: break; @@ -103,4 +137,12 @@ public void run() { } } } + + private Object deepCopy(Object serializableObject) throws Exception { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new ObjectOutputStream(outputStream).writeObject(serializableObject); + ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray()); + + return new ObjectInputStream(inputStream).readObject(); + } } diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Util.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Util.java index d5decaf..2f1e85b 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Util.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/Util.java @@ -117,4 +117,13 @@ protected static boolean isEmulator() { protected static boolean isDebuggerAttached() { return Debug.isDebuggerConnected(); } + + /** + * Determines if Lifecycle Tracking is available for the current user or not. + * + * @return YES if app runs on at least OS 4.0 + */ + protected static boolean isLifecycleTrackingAvailable() { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH); + } } diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/config/IQueueConfig.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/config/IQueueConfig.java index f6cb3c8..8668009 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/config/IQueueConfig.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/library/config/IQueueConfig.java @@ -10,13 +10,13 @@ public interface IQueueConfig { /** * Sets the maximum size of a batch in bytes - * @param maxBatchCount the batchsize of data that will be queued until we send/persist it + * @param maxBatchCount the batchsize of data that will be queued until we send/writeToDisk it */ public void setMaxBatchCount(int maxBatchCount); /** * Gets the maximum interval allowed between calls to batchInvoke - * @return the interval until we send/persist queued up data + * @return the interval until we send/writeToDisk queued up data */ public int getMaxBatchIntervalMs(); diff --git a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/logging/InternalLogging.java b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/logging/InternalLogging.java index 51eeac7..d472eca 100644 --- a/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/logging/InternalLogging.java +++ b/applicationinsights-android/src/main/java/com/microsoft/applicationinsights/logging/InternalLogging.java @@ -13,7 +13,7 @@ private InternalLogging() { /** * Inform SDK users about SDK activities. This has 3 parameters to avoid the string - * concatenation then verbose mode is disabled. + * concatenation when verbose mode is disabled. * * @param tag the log context * @param message the log message @@ -26,11 +26,24 @@ public static void info(String tag, String message, String payload) { } /** - * Warn SDK users about non-critical SDK misuse + * Inform SDK users about SDK activities. * * @param tag the log context * @param message the log message */ + public static void info(String tag, String message) { + if (ApplicationInsights.isDeveloperMode()) { + Log.i(PREFIX + " " + tag, message); + } + } + + + /** + * Warn SDK users about non-critical SDK misuse + * + * @param tag the log context + * @param message the log message + */ public static void warn(String tag, String message) { if (ApplicationInsights.isDeveloperMode()) { Log.w(PREFIX + " " + tag, message); diff --git a/build.gradle b/build.gradle index 2f4add0..72c842d 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.1.0' + classpath 'com.android.tools.build:gradle:1.2.3' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0' classpath 'com.github.dcendents:android-maven-plugin:1.2' } diff --git a/gradle.properties b/gradle.properties index 3461bc5..f83138f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ # org.gradle.parallel=true projectGroup=com.microsoft.azure -projectVersion=1.0-beta.4 +projectVersion=1.0-beta.5 projectRepo=https://github.com/Microsoft/ApplicationInsights-Android projectName=ApplicationInsights-Android projectDesc=Application Insights SDK for Android