diff --git a/codecov.yml b/codecov.yml index 77707aa1..7e29772e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,2 +1,7 @@ github_checks: annotations: false + +coverage: + status: + project: off + patch: off diff --git a/parsely/build.gradle b/parsely/build.gradle index 5062f878..811a02af 100644 --- a/parsely/build.gradle +++ b/parsely/build.gradle @@ -78,6 +78,7 @@ dependencies { androidTestImplementation "org.assertj:assertj-core:$assertJVersion" androidTestImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion" androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestUtil 'androidx.test:orchestrator:1.4.2' } diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt index 8c939c6d..6846e6fc 100644 --- a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt +++ b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt @@ -3,6 +3,9 @@ package com.parsely.parselyandroid import android.app.Activity import androidx.test.core.app.ActivityScenario import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.type.TypeReference @@ -14,6 +17,8 @@ import java.lang.reflect.Field import java.nio.file.Path import java.util.concurrent.TimeUnit import kotlin.io.path.Path +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.runBlocking @@ -90,13 +95,13 @@ class FunctionalTests { parselyTracker.trackPageview("url", null, null, null) } - Thread.sleep((flushInterval / 2).inWholeMilliseconds) + Thread.sleep((defaultFlushInterval / 2).inWholeMilliseconds) scenario.onActivity { parselyTracker.trackPageview("url", null, null, null) } - Thread.sleep((flushInterval / 2).inWholeMilliseconds) + Thread.sleep((defaultFlushInterval / 2).inWholeMilliseconds) val firstRequestPayload = server.takeRequest(2000, TimeUnit.MILLISECONDS)?.toMap() assertThat(firstRequestPayload!!["events"]).hasSize(2) @@ -105,13 +110,50 @@ class FunctionalTests { parselyTracker.trackPageview("url", null, null, null) } - Thread.sleep(flushInterval.inWholeMilliseconds) + Thread.sleep(defaultFlushInterval.inWholeMilliseconds) val secondRequestPayload = server.takeRequest(2000, TimeUnit.MILLISECONDS)?.toMap() assertThat(secondRequestPayload!!["events"]).hasSize(1) } } + /** + * In this scenario, the consumer application: + * 1. Goes to the background + * 2. Is re-launched + * This pattern occurs twice, which allows us to confirm the following assertions: + * 1. The event request is triggered when the consumer application is moved to the background + * 2. If the consumer application is sent to the background again within a short interval, + * the request is not duplicated. + */ + @Test + fun appSendsEventsWhenMovedToBackgroundAndDoesntSendDuplicatedRequestWhenItsMovedToBackgroundAgainQuickly() { + val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + ActivityScenario.launch(SampleActivity::class.java).use { scenario -> + scenario.onActivity { activity: Activity -> + beforeEach(activity) + server.enqueue(MockResponse().setResponseCode(200)) + server.enqueue(MockResponse().setResponseCode(200)) + parselyTracker = initializeTracker(activity, flushInterval = 1.hours) + + repeat(20) { + parselyTracker.trackPageview("url", null, null, null) + } + } + + device.pressHome() + device.pressRecentApps() + device.findObject(UiSelector().descriptionContains("com.parsely")).click() + device.pressHome() + + val firstRequest = server.takeRequest(10000, TimeUnit.MILLISECONDS)?.toMap() + val secondRequest = server.takeRequest(10000, TimeUnit.MILLISECONDS)?.toMap() + + assertThat(firstRequest!!["events"]).hasSize(20) + assertThat(secondRequest).isNull() + } + } + private fun RecordedRequest.toMap(): Map> { val listType: TypeReference>> = object : TypeReference>>() {} @@ -132,7 +174,10 @@ class FunctionalTests { } } - private fun initializeTracker(activity: Activity): ParselyTracker { + private fun initializeTracker( + activity: Activity, + flushInterval: Duration = defaultFlushInterval + ): ParselyTracker { return ParselyTracker.sharedInstance( siteId, flushInterval.inWholeSeconds.toInt(), activity.application ).apply { @@ -145,7 +190,7 @@ class FunctionalTests { private companion object { const val siteId = "123" const val localStorageFileName = "parsely-events.ser" - val flushInterval = 5.seconds + val defaultFlushInterval = 5.seconds } class SampleActivity : Activity() diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java b/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java deleted file mode 100644 index 992fb973..00000000 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2016 Parse.ly, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -package com.parsely.parselyandroid; - -import android.os.AsyncTask; - -import androidx.annotation.NonNull; - -import java.io.OutputStream; -import java.net.URL; -import java.net.HttpURLConnection; - -class ParselyAPIConnection extends AsyncTask { - - @NonNull - private final ParselyTracker tracker; - private Exception exception; - - ParselyAPIConnection(@NonNull ParselyTracker tracker) { - this.tracker = tracker; - } - - @Override - protected Void doInBackground(String... data) { - HttpURLConnection connection = null; - try { - if (data.length == 1) { // non-batched (since no post data is included) - connection = (HttpURLConnection) new URL(data[0]).openConnection(); - connection.getInputStream(); - } else if (data.length == 2) { // batched (post data included) - connection = (HttpURLConnection) new URL(data[0]).openConnection(); - connection.setDoOutput(true); // Triggers POST (aka silliest interface ever) - connection.setRequestProperty("Content-Type", "application/json"); - - OutputStream output = connection.getOutputStream(); - output.write(data[1].getBytes()); - output.close(); - connection.getInputStream(); - } - - } catch (Exception ex) { - this.exception = ex; - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if (this.exception != null) { - ParselyTracker.PLog("Pixel request exception"); - ParselyTracker.PLog(this.exception.toString()); - } else { - ParselyTracker.PLog("Pixel request success"); - - // only purge the queue if the request was successful - tracker.purgeEventsQueue(); - - ParselyTracker.PLog("Event queue empty, flush timer cleared."); - tracker.stopFlushTimer(); - } - } -} diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.kt b/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.kt new file mode 100644 index 00000000..8d0634bd --- /dev/null +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.kt @@ -0,0 +1,62 @@ +/* + Copyright 2016 Parse.ly, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +@file:Suppress("DEPRECATION") +package com.parsely.parselyandroid + +import android.os.AsyncTask +import java.net.HttpURLConnection +import java.net.URL + +internal class ParselyAPIConnection(private val tracker: ParselyTracker) : AsyncTask() { + private var exception: Exception? = null + + @Deprecated("Deprecated in Java") + override fun doInBackground(vararg data: String?): Void? { + var connection: HttpURLConnection? = null + try { + if (data.size == 1) { // non-batched (since no post data is included) + connection = URL(data[0]).openConnection() as HttpURLConnection + connection.inputStream + } else if (data.size == 2) { // batched (post data included) + connection = URL(data[0]).openConnection() as HttpURLConnection + connection.doOutput = true // Triggers POST (aka silliest interface ever) + connection.setRequestProperty("Content-Type", "application/json") + val output = connection.outputStream + output.write(data[1]?.toByteArray()) + output.close() + connection.inputStream + } + } catch (ex: Exception) { + exception = ex + } + return null + } + + @Deprecated("Deprecated in Java") + override fun onPostExecute(result: Void?) { + if (exception != null) { + ParselyTracker.PLog("Pixel request exception") + ParselyTracker.PLog(exception.toString()) + } else { + ParselyTracker.PLog("Pixel request success") + + // only purge the queue if the request was successful + tracker.purgeEventsQueue() + ParselyTracker.PLog("Event queue empty, flush timer cleared.") + tracker.stopFlushTimer() + } + } +}