-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add Pixels when app restarts in foreground by automatic data clearer #858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3b63afb
3d89128
c103abd
cc00e63
fc6b454
24f1479
329a99a
5fef399
47ae684
c7a309f
65455a8
9191d98
9bc638c
dd9a07c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /* | ||
| * Copyright (c) 2020 DuckDuckGo | ||
| * | ||
| * 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.duckduckgo.app.fire | ||
|
|
||
| import android.content.Intent | ||
| import android.net.Uri | ||
| import androidx.test.platform.app.InstrumentationRegistry | ||
| import com.duckduckgo.app.browser.BrowserActivity | ||
| import com.duckduckgo.app.statistics.pixels.Pixel | ||
| import com.duckduckgo.app.systemsearch.SystemSearchActivity | ||
| import com.nhaarman.mockitokotlin2.mock | ||
| import com.nhaarman.mockitokotlin2.verify | ||
| import org.junit.Test | ||
|
|
||
| class DataClearerForegroundAppRestartPixelTest { | ||
|
|
||
| private val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
| private val pixel = mock<Pixel>() | ||
| private val testee = DataClearerForegroundAppRestartPixel(context, pixel) | ||
|
|
||
| @Test | ||
| fun whenAppRestartsAfterOpenSearchWidgetThenPixelWithIntentIsSent() { | ||
| val intent = SystemSearchActivity.fromWidget(context) | ||
| testee.registerIntent(intent) | ||
| testee.incrementCount() | ||
|
|
||
| testee.firePendingPixels() | ||
|
|
||
| verify(pixel).fire(Pixel.PixelName.FORGET_ALL_AUTO_RESTART_WITH_INTENT) | ||
| } | ||
|
|
||
| @Test | ||
| fun whenAppRestartsAfterOpenExternalLinkThenPixelWithIntentIsSent() { | ||
| val i = givenIntentWithData("https://example.com") | ||
| testee.registerIntent(i) | ||
| testee.incrementCount() | ||
|
|
||
| testee.firePendingPixels() | ||
|
|
||
| verify(pixel).fire(Pixel.PixelName.FORGET_ALL_AUTO_RESTART_WITH_INTENT) | ||
| } | ||
|
|
||
| @Test | ||
| fun whenAppRestartsAfterOpenAnEmptyIntentThenPixelIsSent() { | ||
| val intent = givenEmptyIntent() | ||
| testee.registerIntent(intent) | ||
| testee.incrementCount() | ||
|
|
||
| testee.firePendingPixels() | ||
|
|
||
| verify(pixel).fire(Pixel.PixelName.FORGET_ALL_AUTO_RESTART) | ||
| } | ||
|
|
||
| @Test | ||
| fun whenAllUnsentPixelsAreFiredThenResetCounter() { | ||
| val intent = givenEmptyIntent() | ||
| testee.registerIntent(intent) | ||
| testee.incrementCount() | ||
|
|
||
| testee.firePendingPixels() | ||
| testee.firePendingPixels() | ||
|
|
||
| verify(pixel).fire(Pixel.PixelName.FORGET_ALL_AUTO_RESTART) | ||
| } | ||
|
|
||
| @Test | ||
| fun whenAppRestartedAfterGoingBackFromBackgroundThenPixelIsSent() { | ||
| val intent = SystemSearchActivity.fromWidget(context) | ||
| testee.registerIntent(intent) | ||
| testee.onAppBackgrounded() | ||
| testee.incrementCount() | ||
|
|
||
| testee.firePendingPixels() | ||
|
|
||
| verify(pixel).fire(Pixel.PixelName.FORGET_ALL_AUTO_RESTART) | ||
| } | ||
|
|
||
| private fun givenEmptyIntent(): Intent = Intent(context, BrowserActivity::class.java) | ||
|
|
||
| private fun givenIntentWithData(url: String) = Intent(Intent.ACTION_VIEW).apply { | ||
| data = Uri.parse(url) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /* | ||
| * Copyright (c) 2020 DuckDuckGo | ||
| * | ||
| * 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.duckduckgo.app.fire | ||
|
|
||
| import android.content.Context | ||
| import android.content.Intent | ||
| import android.content.SharedPreferences | ||
| import androidx.annotation.UiThread | ||
| import androidx.annotation.VisibleForTesting | ||
| import androidx.core.content.edit | ||
| import androidx.lifecycle.Lifecycle | ||
| import androidx.lifecycle.LifecycleObserver | ||
| import androidx.lifecycle.OnLifecycleEvent | ||
| import com.duckduckgo.app.global.intentText | ||
| import com.duckduckgo.app.statistics.pixels.Pixel | ||
| import com.duckduckgo.app.systemsearch.SystemSearchActivity | ||
| import timber.log.Timber | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| /** | ||
| * Stores information about unsent automatic data clearer restart Pixels, detecting if user started the app from an external Intent. | ||
| * Contains logic to send unsent pixels. | ||
| * | ||
| * When writing values here to SharedPreferences, it is crucial to use `commit = true`. As otherwise the change can be lost in the process restart. | ||
| */ | ||
| @Singleton | ||
| class DataClearerForegroundAppRestartPixel @Inject constructor( | ||
| private val context: Context, | ||
| private val pixel: Pixel | ||
| ) : LifecycleObserver { | ||
| private var detectedUserIntent: Boolean = false | ||
|
|
||
| private val pendingAppForegroundRestart: Int | ||
| get() = preferences.getInt(KEY_UNSENT_CLEAR_APP_RESTARTED_PIXELS, 0) | ||
|
|
||
| private val pendingAppForegroundRestartWithIntent: Int | ||
| get() = preferences.getInt(KEY_UNSENT_CLEAR_APP_RESTARTED_WITH_INTENT_PIXELS, 0) | ||
|
|
||
| @UiThread | ||
| @OnLifecycleEvent(Lifecycle.Event.ON_STOP) | ||
| fun onAppBackgrounded() { | ||
| Timber.i("Registered App on_stop") | ||
| detectedUserIntent = false | ||
| } | ||
|
|
||
| fun registerIntent(intent: Intent?) { | ||
| detectedUserIntent = widgetActivity(intent) || !intent?.intentText.isNullOrEmpty() | ||
| } | ||
|
|
||
| fun incrementCount() { | ||
| if (detectedUserIntent) { | ||
| Timber.i("Registered restart with intent") | ||
| incrementCount(pendingAppForegroundRestart, KEY_UNSENT_CLEAR_APP_RESTARTED_WITH_INTENT_PIXELS) | ||
| } else { | ||
| Timber.i("Registered restart without intent") | ||
| incrementCount(pendingAppForegroundRestartWithIntent, KEY_UNSENT_CLEAR_APP_RESTARTED_PIXELS) | ||
| } | ||
| } | ||
|
|
||
| fun firePendingPixels() { | ||
| firePendingPixels(pendingAppForegroundRestart, Pixel.PixelName.FORGET_ALL_AUTO_RESTART) | ||
| firePendingPixels(pendingAppForegroundRestartWithIntent, Pixel.PixelName.FORGET_ALL_AUTO_RESTART_WITH_INTENT) | ||
| resetCount() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is called from the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We talked about this offline, and we think it's fine to keep it as it's right now. |
||
| } | ||
|
|
||
| private fun incrementCount(counter: Int, sharedPrefKey: String) { | ||
| val updated = counter + 1 | ||
| preferences.edit(commit = true) { | ||
| putInt(sharedPrefKey, updated) | ||
| } | ||
| } | ||
|
|
||
| private fun firePendingPixels(counter: Int, pixelName: Pixel.PixelName) { | ||
| if (counter > 0) { | ||
| for (i in 1..counter) { | ||
| Timber.i("Fired pixel: ${pixelName.pixelName}/$counter") | ||
| pixel.fire(pixelName) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun resetCount() { | ||
| preferences.edit(commit = true) { | ||
| putInt(KEY_UNSENT_CLEAR_APP_RESTARTED_PIXELS, 0) | ||
| putInt(KEY_UNSENT_CLEAR_APP_RESTARTED_WITH_INTENT_PIXELS, 0) | ||
| } | ||
| Timber.i("counter reset") | ||
| } | ||
|
|
||
| private fun widgetActivity(intent: Intent?): Boolean = | ||
| intent?.component?.className?.contains(SystemSearchActivity::class.java.canonicalName.orEmpty()) == true | ||
|
|
||
| private val preferences: SharedPreferences by lazy { | ||
| context.getSharedPreferences(FILENAME, Context.MODE_PRIVATE) | ||
| } | ||
|
|
||
| companion object { | ||
| @VisibleForTesting | ||
| const val FILENAME = "com.duckduckgo.app.fire.unsentpixels.settings" | ||
| const val KEY_UNSENT_CLEAR_APP_RESTARTED_PIXELS = "KEY_UNSENT_CLEAR_APP_RESTARTED_PIXELS" | ||
| const val KEY_UNSENT_CLEAR_APP_RESTARTED_WITH_INTENT_PIXELS = "KEY_UNSENT_CLEAR_APP_RESTARTED_WITH_INTENT_PIXELS" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ import com.duckduckgo.app.di.DaggerAppComponent | |
| import com.duckduckgo.app.fire.DataClearer | ||
| import com.duckduckgo.app.fire.FireActivity | ||
| import com.duckduckgo.app.fire.UnsentForgetAllPixelStore | ||
| import com.duckduckgo.app.fire.DataClearerForegroundAppRestartPixel | ||
| import com.duckduckgo.app.global.Theming.initializeTheme | ||
| import com.duckduckgo.app.global.initialization.AppDataLoader | ||
| import com.duckduckgo.app.global.install.AppInstallStore | ||
|
|
@@ -110,6 +111,9 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO | |
| @Inject | ||
| lateinit var unsentForgetAllPixelStore: UnsentForgetAllPixelStore | ||
|
|
||
| @Inject | ||
| lateinit var dataClearerForegroundAppRestartPixel: DataClearerForegroundAppRestartPixel | ||
|
|
||
| @Inject | ||
| lateinit var offlinePixelScheduler: OfflinePixelScheduler | ||
|
|
||
|
|
@@ -170,6 +174,7 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO | |
| it.addObserver(appDaysUsedRecorder) | ||
| it.addObserver(defaultBrowserObserver) | ||
| it.addObserver(appEnjoymentLifecycleObserver) | ||
| it.addObserver(dataClearerForegroundAppRestartPixel) | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, they are equivalent, but no strong preference on styles here. So for now, it's fine to keep it as it is now and focus in related changes to our initial goal. But that was a nice catch. 👍 |
||
|
|
||
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { | ||
|
|
@@ -254,6 +259,8 @@ open class DuckDuckGoApplication : HasAndroidInjector, Application(), LifecycleO | |
| } | ||
| unsentForgetAllPixelStore.resetCount() | ||
| } | ||
|
|
||
| dataClearerForegroundAppRestartPixel.firePendingPixels() | ||
| } | ||
|
|
||
| /** | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we find a better name for this? it's not clear from the name what it does or where/when to call it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could come up with a better name, like I previously expected. No problem since this will be removed once we work on the fix at some point.